← Blog

Uvicorn: A Practical Resource for Async Python Developers

Published on February 16, 2026

By ToolsGuruHub

If you're building APIs with FastAPI, Starlette, or any async Python framework, you've probably run uvicorn main:app --reload. But what is Uvicorn, when is it enough on its own, and when do you need Gunicorn in front of it? Here's a clear, honest resource that answers what developers actually need to know.

What is Uvicorn?

Uvicorn is a fast ASGI web server for Python. It listens on a port, accepts HTTP connections, parses them, and runs your async application. If you're using FastAPI or Starlette, Uvicorn is the server that executes your app.

It's built on uvloop (a high-performance event loop, same family as Node's libuv), httptools (fast C-based HTTP parsing), and Python's asyncio. In other words, it's the thing that actually runs your async code when a request hits your API. It plays the same role for async Python that Gunicorn plays for traditional WSGI apps - but it's designed for async workloads from the ground up.

What is ASGI?

ASGI (Asynchronous Server Gateway Interface) is the successor to WSGI. WSGI was built for synchronous request/response and one request per worker. ASGI was designed for async/await, WebSockets, long-lived connections, streaming, and background tasks.

Uvicorn Server (ASGI) architecture and WSGI vs ASGI comparison

The core difference:

  • WSGI: request → block → response. The worker is busy until the response is done.
  • ASGI: request → coroutine → event loop → non-blocking I/O → response. While one request is waiting on a database or an external API, the worker can handle others.

ASGI apps are coroutine-based. They don't block the entire worker when waiting on I/O. That's why FastAPI and Starlette require an ASGI server like Uvicorn.

How Async Event Loops Work

Async does not mean parallel. It means cooperative multitasking. Uvicorn runs a single-threaded event loop (via asyncio or uvloop). The loop accepts connections, schedules coroutines, and switches between them whenever they await.

When your code does await db.fetch(), it yields control back to the event loop. The loop then runs another coroutine while the first is waiting on I/O. When the database responds, the loop resumes the first coroutine. So one thread can juggle many connections: if 1,000 requests are waiting on I/O, Uvicorn keeps one thread and switches between them instead of spawning 1,000 threads. That's why async servers scale well for I/O-heavy workloads, and why blocking the loop with CPU work hurts everyone.

Performance Profile

Uvicorn performs very well when workloads are I/O-bound, most operations use await, and CPU use per request is low. It shines for REST APIs, WebSockets, streaming, and high-concurrency, low-CPU endpoints. It struggles with CPU-heavy tasks, large synchronous codebases, blocking libraries (e.g. non-async DB drivers), and heavy data processing inside endpoints.

one Uvicorn worker = one event loop = one CPU core If you block it with CPU work, everything slows down. Never use time.sleep() or heavy synchronous computation in an async route. Use await asyncio.sleep() and offload CPU work to a thread pool, process pool, or task queue.

When to Use Uvicorn Alone

You can run Uvicorn directly in production:

uvicorn main:app --host 0.0.0.0 --port 8000

Use Uvicorn alone when you're deploying in Docker or Kubernetes, scaling with replicas instead of in-process workers, and running behind a load balancer (Nginx, ALB, etc.). In many modern setups, one container = one Uvicorn process. The orchestrator handles restarts, health checks, and horizontal scaling. You don't need Gunicorn for process management in that case.

When to Use Uvicorn with Gunicorn

You may see:

gunicorn -k uvicorn.workers.UvicornWorker main:app --workers 4

Uvicorn is the async server. Gunicorn is a process manager. Combining them makes sense when you're on bare metal or a single VM without container orchestration. You want multiple workers per machine, automatic worker restart, and graceful reloads. Gunicorn adds a pre-fork model, worker supervision, and signal handling. Uvicorn workers handle the async I/O inside each process.

In Kubernetes, this combo is usually unnecessary. The platform already manages restarts and scaling. Running Gunicorn inside a pod is often redundant.

Limitations

  • Single process by default. Uvicorn doesn't manage multiple processes unless you use --workers N, and that's basic multiprocessing, not as robust as Gunicorn's pre-fork model.
  • CPU-bound work is dangerous. Blocking the event loop with synchronous CPU work stalls all requests. Use background workers, process pools, or task queues for heavy computation.
  • Blocking libraries kill performance. Non-async DB drivers, blocking HTTP clients, or time.sleep() destroy concurrency. Async only helps if the stack underneath is async too.
  • Not a reverse proxy. Uvicorn doesn't terminate TLS at scale, do advanced routing, or cache. Run it behind Nginx, a cloud load balancer, or an API gateway.

Common Misunderstandings

  • Async means faster. No. Async means better concurrency and more efficient I/O handling. If your app is CPU-heavy, async won't fix that.
  • Uvicorn replaces Gunicorn. They solve different problems: Uvicorn is the ASGI server (async execution); Gunicorn is a process manager. They can be combined; they're not direct competitors.
  • More workers = more performance. Not always. Too many workers increase memory and context switching. For I/O-bound apps, fewer workers plus async concurrency is often better; for CPU-bound, workers ≈ CPU cores is a reasonable starting point.
  • Async fixes bad architecture. It doesn't. Slow databases, inefficient queries, or heavy CPU logic stay slow. Async helps with I/O scheduling, not design flaws.

Summary

Think of Uvicorn as a high-performance async engine that runs your FastAPI (or other ASGI) app on an event loop. It excels at high concurrency and I/O-heavy workloads. It struggles with CPU-heavy work and blocking code. Used correctly, it's simple and production-ready. Most production issues with Uvicorn are not Uvicorn bugs. They're blocking-code or architectural issues. Use Uvicorn alone when you're in containers and orchestrators; use it with Gunicorn when you're on a VM and want process management. Put something in front of it (Nginx, load balancer) for TLS and robustness. That's the honest picture.