Back to Blog
Python

Running Python Gunicorn with Uvicorn Workers

Run FastAPI and other ASGI apps with Gunicorn and Uvicorn workers. This guide covers worker class setup, concurrency, timeouts, and production pitfalls.

GunicornUvicornASGIFastAPIDeployment
Diagram showing Gunicorn as a process manager delegating ASGI requests to Uvicorn workers.

When you deploy a FastAPI or Starlette application, you may want Gunicorn's process management, signal handling, and graceful restarts. Gunicorn, however, is a WSGI server by default, while an ASGI application expects an ASGI server. A common production setup is Gunicorn with Uvicorn workers: Gunicorn manages the process lifecycle, and Uvicorn's worker class provides the ASGI interface and an event loop.

Why Gunicorn Needs Uvicorn Workers

Gunicorn's default worker type is a synchronous WSGI worker. It calls a WSGI application with an environ dict and a start_response callback, then waits for the response to be returned. ASGI applications use a different asynchronous protocol and may rely on WebSockets or long-lived connections. Uvicorn implements the ASGI layer. Running Uvicorn directly works, but it means you handle process management yourself. Combining Gunicorn and Uvicorn gives you Gunicorn's mature process supervision with Uvicorn's ASGI support.

Setting Up the Worker Class

The simplest way to use Uvicorn workers with Gunicorn is to select the worker class on the command line:

gunicorn -k uvicorn.workers.UvicornWorker myapp:app

myapp:app points to the ASGI application instance, usually defined in a Python module. The -k flag selects the worker class. Uvicorn provides two worker classes: UvicornWorker for the standard ASGI implementation and UvicornH11Worker for using the h11 protocol implementation instead of httptools. Most applications work with the default UvicornWorker.

You can also put this configuration in a Gunicorn config file:

# gunicorn.conf.py worker_class = 'uvicorn.workers.UvicornWorker' bind = '0.0.0.0:8000' workers = 4

Then run:

gunicorn -c gunicorn.conf.py myapp:app

Using a config file makes the setup reproducible across environments and keeps command-line flags short.

How Uvicorn Workers Handle Concurrency

Each Gunicorn worker process runs its own Uvicorn server with an event loop. Within that loop, asynchronous tasks are interleaved. This means a single Uvicorn worker can handle many concurrent requests if they are I/O-bound and use await properly. Blocking calls, such as synchronous database drivers or CPU-heavy computations, block the event loop and stall all requests handled by that worker.

Gunicorn's workers setting controls the number of worker processes. The default is 1, which is often too low for production. A common starting formula is 2 * CPU cores + 1, but the right number depends on your workload. Each worker consumes memory, so more workers increase memory usage. An async, I/O-bound application may need fewer workers than a synchronous WSGI app.

Configuring Timeouts and Keep-Alive

Gunicorn's default timeout is 30 seconds. With Uvicorn workers, this timeout applies to a worker that has been silent for too long, so a long-running request or a blocked event loop can trigger Worker timed out in the logs. Increase it with --timeout or the timeout config option if you have legitimate long-running work, but avoid using a high timeout to hide performance problems.

The keepalive setting controls how long an HTTP/1.1 connection stays open after a request completes. A longer keep-alive can reduce connection setup overhead. Gunicorn's keepalive option defaults to 2 seconds.

Performance Considerations and Worker Count

The main performance trade-off is between process-level parallelism and event-loop concurrency. Uvicorn workers give you both: multiple processes, each with an event loop that can handle many concurrent connections. The GIL still limits CPU-bound parallelism inside one process, so CPU-heavy work needs more worker processes to use multiple cores. If your application is I/O-bound, a few workers with a well-tuned event loop can handle many connections.

A common mistake is setting workers too high, which leads to memory exhaustion and context-switching overhead. Start with a modest number, measure CPU and memory usage under load, and adjust.

If you need to run synchronous or blocking code, do not assume that Gunicorn's --threads option creates a thread pool inside an Uvicorn worker; that option is designed for Gunicorn's threaded worker class. In a FastAPI or Starlette app, offload blocking work with asyncio.to_thread or a dedicated executor, or scale out with more Uvicorn worker processes.

Common Pitfalls and Troubleshooting

One frequent issue is forgetting that Gunicorn's timeout applies to the whole request cycle. If logs show Worker timed out, a request may have taken longer than the configured timeout, possibly because the event loop was blocked. Look for synchronous database calls or CPU-bound loops that do not yield control.

Another pitfall is using file-based reload (--reload) in production. Reload is intended for development and can cause unexpected restarts. Make sure reload is disabled in production.

To support WebSockets, use UvicornWorker rather than a plain WSGI worker. When running behind a reverse proxy, configure forwarded_allow_ips or the proxy protocol for trusted proxies only, so your app sees the client's real IP address. This matters for logging and rate limiting.

Alternative Approaches

Running Uvicorn directly is simpler:

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

This gives the same ASGI behavior but does not include Gunicorn's process management. For a single-process deployment, Uvicorn alone may be enough. For multiple processes, uvicorn --workers 4 uses a similar process model, while Gunicorn offers mature signal handling and configuration options.

If you need HTTP/2 or other features not covered here, consider running an alternative ASGI server such as Hypercorn. Gunicorn with Uvicorn workers remains a common, well-documented production setup for FastAPI applications.

Final Configuration Example

A starting Gunicorn config for a typical FastAPI app might look like this:

# gunicorn.conf.py import multiprocessing bind = '0.0.0.0:8000' workers = multiprocessing.cpu_count() * 2 + 1 worker_class = 'uvicorn.workers.UvicornWorker' timeout = 60 keepalive = 5 forwarded_allow_ips = '*'

This sets the number of workers from CPU count, uses Uvicorn workers, and adjusts timeouts for slower requests. The forwarded_allow_ips = '*' value is only safe when you control the reverse proxy; otherwise restrict it to trusted IPs. Adjust these values after load testing and measuring your application's actual behavior.

Gunicorn with Uvicorn Workers: Setup, Concurrency, and Production Tips | RYUSLOG DEV