Uvicorn, Gunicorn, and Workers

RoadmapsPython Backend

Scenario

You deploy your blazing-fast async FastAPI app using `uvicorn app.main:app`. Under load, it maxes out exactly 1 CPU core at 100%, while the other 7 cores sit completely idle.

Mental model

Uvicorn is a chef working on a single stove (one process, one event loop). No matter how fast they juggle orders asynchronously, they only have two hands. Gunicorn is a restaurant manager that hires N chefs (worker processes) so you can utilize the whole kitchen.

Deep dive

Because of Python's Global Interpreter Lock (GIL), a single Python process can only execute on one CPU core at a time, regardless of how many async tasks or threads it spins up.

Uvicorn is an ASGI server. It runs an event loop in a single process. It handles concurrent I/O brilliantly, but it cannot utilize multiple CPU cores.

To scale across cores, we use Gunicorn as a process manager. Gunicorn forks the application into multiple independent worker processes, each running an instance of Uvicorn. This bypasses the GIL entirely.

**Gunicorn worker classes** — By default, Gunicorn uses synchronous workers that block on I/O. To run an async framework like FastAPI, you must tell Gunicorn to use the Uvicorn worker class (`-k uvicorn.workers.UvicornWorker`). This gives you Gunicorn's multi-process scaling combined with Uvicorn's async event loop inside each process.

Specify the Uvicorn worker class so each Gunicorn process can handle async I/O.

**How many workers?** — The standard heuristic is `(2 x $num_cores) + 1`. For a 4-core machine, you run 9 workers. This ensures that even if some workers are busy or blocked, others are ready to accept new requests. If your app is heavily CPU-bound, a 1:1 ratio with cores might be more efficient.

Run (2 × cores) + 1 workers to maximize throughput.

Code examples

Development Server (Single Core)

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

This runs one process. Fine for development, but in production, it will bottleneck on a single CPU core.

Production Server (Ground Truth)

gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

Gunicorn acts as the master process. `-w 4` creates 4 separate Python processes. `-k` tells them to use the Uvicorn ASGI worker class. 4 event loops, 4 CPU cores utilized.

Common mistakes

Recall questions

Questions & answers

If you have N Gunicorn workers, how does memory usage scale?

Memory scales linearly (N × memory). Since they are independent processes, they do not share memory space. Every worker loads the entire application and its dependencies into RAM.

A request taking 2 seconds of heavy CPU calculation brings your entire API to a halt. You are using Gunicorn with 4 workers. Why did it halt, and how do you fix it?

CPU-bound work blocks the asyncio event loop. With 4 workers, if 4 users make that request, all 4 event loops are blocked. The fix is to offload the CPU-bound task to a background queue (like Celery/Redis) rather than doing it in the API request lifecycle.

Continue learning

Previous: WSGI vs ASGI

Return to Python Backend Roadmap