Gunicorn and Uvicorn are ASGI/WSGI servers that run your Django application. While they’re often considered drop-in replacements for the development server, their configuration, particularly the number and type of worker processes, can dramatically impact your application’s performance and stability. The key is to understand that they’re not just running your code; they’re managing concurrency, and that management has trade-offs.
Here’s a Django app running with Uvicorn, showing a basic request-response cycle and how worker configuration affects it.
# main.py
import uvicorn
from django.core.asgi import get_asgi_application
# Set up Django settings
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
application = get_asgi_application()
# Simulate a view that takes some time
async def homepage(scope, receive, send):
assert scope['type'] == 'http'
from time import sleep
sleep(0.1) # Simulate blocking I/O
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'text/plain'],
],
})
await send({
'type': 'http.response.body',
'body': b'Hello, world!',
})
if __name__ == "__main__":
# This is a simplified example. In a real Django project,
# you'd typically run this via `uvicorn myproject.asgi:application --reload`
# For this demonstration, we'll directly run the simulate view.
# In a real Django app, the `application` above would handle routing.
uvicorn.run(homepage, host="127.0.0.1", port=8000)
If you were to run this with uvicorn main:app --workers 1, and then hit it with 5 concurrent requests, you’d see each request wait for the previous one to finish because the single worker is busy for 0.1 seconds per request. If you increase --workers to 4, you’ll see requests starting to be processed in parallel, significantly reducing the overall latency for the batch.
The "workers" in Gunicorn and Uvicorn aren’t just threads; they are actual operating system processes. This means each worker has its own Python interpreter and memory space. When you configure workers=4, you’re telling the server to spawn four independent Python processes, each capable of handling a request. This is crucial for I/O-bound tasks, like database queries or external API calls, because one worker can be waiting for an external resource while other workers handle different requests.
However, this multiprocessing comes at a cost: increased memory consumption. Each worker process consumes memory, so having too many workers can lead to your server exhausting its RAM, causing swapping and severe performance degradation, or even crashes. The default worker type for Uvicorn is uvicorn.workers.UvicornWorker (which is asynchronous) and for Gunicorn, it’s gunicorn.workers.SyncWorker (which is synchronous). For Django, which traditionally uses WSGI (synchronous), SyncWorker is often the default and works well. However, with modern Django and ASGI, using Uvicorn or Gunicorn with an ASGI worker class can unlock better concurrency for I/O-bound tasks.
The magic number for worker processes is often cited as (2 * number_of_cpu_cores) + 1. This heuristic aims to keep your CPU cores busy while also accounting for I/O wait times. If you have a CPU-bound Django application (e.g., heavy computation, image processing within the request), you might not see significant benefits from more than one worker per CPU core. For I/O-bound applications, the +1 is for the master process and potentially for handling spikes in traffic.
Let’s look at tuning for a hypothetical Django app on a server with 4 CPU cores.
Scenario 1: I/O Bound Application (e.g., lots of database queries, external API calls)
- Gunicorn:
- Command:
gunicorn myproject.asgi:application --workers 9 --worker-class gunicorn.workers.UvicornWorker --bind 0.0.0.0:8000 - Explanation: We use
9workers (2*4 + 1). Crucially,--worker-class gunicorn.workers.UvicornWorkertells Gunicorn to use Uvicorn’s event loop within each worker, allowing for better handling of asynchronous I/O.
- Command:
- Uvicorn:
- Command:
uvicorn myproject.asgi:application --workers 4 --loop uvloop --http httptools --bind 0.0.0.0:8000 - Explanation: Here, we use
4workers. Uvicorn’s default is to use asynchronous workers.uvloopandhttptoolsare performance-oriented libraries that can further speed up I/O and parsing. While(2 * cores) + 1is a common starting point, for pure ASGI with Uvicorn, simply matching the number of cores (4in this case) can be very effective because each worker can handle multiple concurrent requests internally due to its async nature.
- Command:
Scenario 2: CPU Bound Application (e.g., heavy data processing, complex calculations)
- Gunicorn:
- Command:
gunicorn myproject.asgi:application --workers 4 --bind 0.0.0.0:8000 - Explanation: We stick to
4workers, matching the CPU cores. UsingSyncWorker(the default) is often fine here as each worker will primarily be CPU-bound. Spawning more workers than cores will likely lead to context switching overhead and reduced performance.
- Command:
- Uvicorn:
- Command:
uvicorn myproject.asgi:application --workers 4 --bind 0.0.0.0:8000 - Explanation: Again,
4workers. While Uvicorn’s async nature is beneficial for I/O, if your tasks are purely CPU-bound, you won’t see much benefit from a single worker trying to juggle many tasks. Each worker process is a separate Python interpreter, and you want those interpreters to run on separate CPU cores.
- Command:
The worker_connections setting (or similar concepts in Uvicorn) is often misunderstood. It doesn’t limit the number of requests a worker can handle, but rather the number of concurrent connections that a worker can manage. For standard HTTP requests, this is usually not a primary tuning knob unless you’re dealing with WebSockets or very long-lived connections.
One critical aspect often overlooked is the impact of synchronous code within an ASGI application. If you have long-running synchronous operations (like time.sleep() or blocking I/O calls not wrapped by sync_to_async or async_to_sync) within your ASGI views, they will block the entire event loop of the worker process. This negates the benefits of asynchronous workers. For Gunicorn with UvicornWorker, or for Uvicorn, ensure your synchronous code is properly offloaded using sync_to_async from asgiref.sync to prevent blocking.
The next rabbit hole you’ll likely fall into is understanding how to configure worker timeouts.