Gunicorn or Uvicorn for Django: Which Server, and When
Gunicorn or Uvicorn for Django: Which Server, and When
Both appear in every deployment guide, often without explaining why you would pick one. The
distinction is not performance in general — it is what happens while your code waits.
The actual difference
Gunicorn is a WSGI server. It runs a pool of worker processes, and each worker handles exactly one
request at a time. While that request waits on the database, the worker waits with it.
Uvicorn is an ASGI server. A single worker can hold many requests concurrently, switching between
them whenever one is waiting. Whether that helps depends entirely on what your requests spend their
time doing.
When async genuinely helps
Concurrency pays off when requests are dominated by waiting on something external — calling a slow
third-party API, holding long-lived connections, streaming responses. During that wait an
asynchronous worker serves other requests instead of sitting idle.
The clearest case is WebSockets. A synchronous worker holding an open connection is a worker doing
nothing else, so a few hundred connected clients exhausts your pool. Asynchronous servers handle
that comfortably.
When it does not
If your requests are short and mostly hit your own database, async adds little. A typical view
queries, renders, returns — the waiting is measured in milliseconds and the concurrency has nothing
to hide.
It can also cost you. Async code is unforgiving about blocking calls: one synchronous database query
or file read inside an async view stalls the entire event loop, and every other request on that
worker waits. A synchronous server has no such trap, because blocking is what it expects.
The combination most people should use
For a conventional Django application, the practical answer is Gunicorn managing Uvicorn workers.
You get Gunicorn's process management — restarts, timeouts, graceful reloads — and ASGI support for
the parts that need it.
gunicorn myapp.asgi:application \
-k uvicorn.workers.UvicornWorker \
--workers 4 --bind 127.0.0.1:8000 --timeout 60
If you use nothing asynchronous at all, plain Gunicorn with the synchronous worker is simpler and
entirely adequate. Simplicity has value.
How many workers
The usual starting point is twice the core count plus one. It is a rule of thumb, not a law: the
right number depends on how much memory each worker uses and how much time requests spend waiting.
Measure rather than guess. Too few workers and requests queue; too many and you exhaust memory or
your database connection limit — every worker holds its own connection, and that adds up faster than
people expect.
What actually makes a site fast
Neither choice will rescue a slow application. In practice, response time is dominated by database
queries, missing indexes and absent caching — usually in that order. A well-indexed application on a
synchronous server outperforms a poorly indexed one on any server you like.
Profile first. Choose the server for the shape of your workload, then spend the remaining effort
where the time actually goes.
For the async side in depth, see Async Django and ASGI in
Production, and for load testing your choice, Load-Testing
and Capacity Planning for Django.