Back to Blog
Python

Python Uvicorn vs Gunicorn: Choosing the Right Server

python uvicorn vs gunicorn: Understand the difference between uvicorn and gunicorn, how they work together, and which one to choose for your Python ASGI or WSGI applic...

uvicorngunicornASGIWSGIFastAPIconcurrency
Diagram comparing uvicorn and gunicorn for Python web applications, showing ASGI and WSGI protocols and process management.

python uvicorn vs gunicorn requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Choosing between uvicorn and gunicorn for a Python web application is not a matter of picking the "best" server. It is about matching the server to the protocol your application speaks and the process model your deployment needs. Uvicorn is an ASGI server that runs async Python code. Gunicorn is a WSGI server with a mature process manager. They are not direct alternatives; they solve different problems, and in many production setups they are used together.

What Uvicorn and Gunicorn Actually Do

Uvicorn is a lightweight ASGI server. It was built to serve async Python frameworks like FastAPI, Starlette, and Django Channels. It uses an event loop, typically asyncio, to handle many concurrent connections in a single process. Uvicorn speaks the ASGI protocol, which supports both HTTP and WebSocket, and it is designed for long-lived connections and high concurrency.

Gunicorn, short for Green Unicorn, is a WSGI server that has been around since 2010. It is a pre-fork worker model server: a master process manages a pool of worker processes, each handling requests. Gunicorn is built for synchronous WSGI applications like Flask and Django. It does not natively understand ASGI, but it can be extended with worker classes that do.

The key distinction is that uvicorn is an application server for ASGI, while gunicorn is a process manager and WSGI server. When you run uvicorn app:app, you get a single process with an event loop. When you run gunicorn app:app, you get a master process and multiple worker processes, each running a WSGI application.

The Protocol Difference: ASGI vs WSGI

WSGI (Web Server Gateway Interface) is the traditional Python web server interface. It is synchronous: each request is handled by a worker that blocks while processing. ASGI (Asynchronous Server Gateway Interface) extends WSGI to support async code, WebSockets, and HTTP/2. ASGI allows a single process to handle many requests concurrently without blocking, which is essential for applications that use async/await or need real-time features.

If your application is built with an async framework like FastAPI, it requires an ASGI server. Running it under a pure WSGI server like gunicorn with the default sync workers will not work, because the framework expects an ASGI interface. Conversely, a traditional Flask application is WSGI-based and does not need ASGI. Trying to serve it with uvicorn alone would require an adapter, and you would lose the benefits of async.

This protocol difference is the primary reason the two servers are often compared. The choice is not uvicorn versus gunicorn in a vacuum; it is about which protocol your application uses and how you want to manage processes.

Running Uvicorn Alone vs Gunicorn with Uvicorn Workers

You can run uvicorn directly:

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

This starts a single uvicorn process with an event loop. It handles all requests concurrently using async I/O. For a small service or a development environment, this is often sufficient.

To run the same application with gunicorn, you need to tell gunicorn to use a worker class that speaks ASGI. Uvicorn provides such a class:

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

Here, gunicorn acts as the process manager. It spawns four worker processes, each running an instance of uvicorn. Each worker has its own event loop, so you get both multiprocessing and async concurrency. This is a common production setup for FastAPI applications.

Why would you want gunicorn on top of uvicorn? Gunicorn provides robust process management: it can restart workers that crash, gracefully handle timeouts, and manage worker lifecycle. Uvicorn also has a --workers flag, but it does not offer the same level of supervision. For example, gunicorn can send signals to workers, reload configuration, and handle worker timeouts in a more mature way.

Concurrency Models: Async vs Multiprocess

Uvicorn's concurrency model is based on a single event loop. Within one process, it can handle thousands of simultaneous connections by multiplexing I/O. This works well for I/O-bound workloads, such as database queries, external API calls, or WebSocket connections. However, a single process is limited to one CPU core. If your application is CPU-bound, you need multiple processes to use multiple cores.

Gunicorn's default worker type is synchronous. Each worker process handles one request at a time, blocking on I/O. This is fine for simple WSGI apps but wasteful for async workloads. When you use UvicornWorker, each gunicorn worker runs an async event loop, so you get the best of both: multiple processes for CPU scaling, and async concurrency within each process.

The decision between uvicorn alone and gunicorn with uvicorn workers often comes down to how many CPU cores you have and how much process supervision you need. If your application is purely I/O-bound and you are running on a single-core container, uvicorn alone may be enough. If you need to scale across cores or want automatic worker restarts, gunicorn is the safer choice.

Production Considerations: Process Management, Timeouts, and Graceful Shutdown

Gunicorn is known for its production-grade process management. It can preload your application, manage worker timeouts, and perform graceful shutdowns. For example, you can set a timeout so that a worker that hangs for too long is killed and replaced:

gunicorn myapp:app -k uvicorn.workers.UvicornWorker --timeout 120 --workers 4

Uvicorn also has a --timeout option, but it applies to request handling, not worker lifecycle. Gunicorn's timeout is a worker timeout: if a worker does not complete a request within the specified seconds, the worker is restarted. This is a critical safety net for production.

Graceful shutdown is another area where gunicorn excels. When you send a SIGTERM to gunicorn, it stops accepting new connections, waits for in-flight requests to finish, and then shuts down workers. Uvicorn also supports graceful shutdown, but gunicorn gives you more control over the process group, especially when you have multiple workers.

If you are running in a containerized environment like Docker or Kubernetes, you often need a process manager to handle signals correctly. Gunicorn is designed for this. Uvicorn alone can work, but you may need to rely on the container runtime to restart it if it crashes.

When to Use Each (Decision Criteria)

Use uvicorn alone when:

  • Your application is ASGI-based and you are running on a single-core environment.
  • You want the simplest possible deployment with no extra process manager.
  • You are in development and need quick iteration without worker management.

Use gunicorn with uvicorn workers when:

  • You need multiple worker processes to utilize multiple CPU cores.
  • You want automatic worker restarts and robust timeout handling.
  • You are deploying to a production environment where uptime matters.

There is also a middle ground: uvicorn with --workers can give you multiple processes, but it does not provide the same level of supervision as gunicorn. If you need process management, gunicorn is the more mature choice.

Common Misconfigurations and How to Avoid Them

One common mistake is running a WSGI app (like Flask) with uvicorn directly. Uvicorn expects an ASGI application. Flask is WSGI, so you would need to use uvicorn with a WSGI middleware like a2wsgi or run it through gunicorn with the default sync workers. The same applies in reverse: running an ASGI app with gunicorn's default workers will fail because the default workers do not understand ASGI.

Another misconfiguration is using --workers with uvicorn without understanding that each worker is a separate process. If your application relies on in-memory state that must be shared across requests, multiple workers will break that assumption. You need an external store like Redis for shared state, regardless of whether you use uvicorn or gunicorn.

Finally, be careful with worker timeouts. If you set a very low timeout on gunicorn, long-running async tasks may be killed prematurely. Uvicorn's async nature means requests can take longer without blocking, but gunicorn's worker timeout is based on wall-clock time. For async applications, you often need to increase the timeout to accommodate long-running requests or background tasks.

Choosing Based on Your Deployment Environment

The final decision often depends on your infrastructure. In a Kubernetes cluster, you might run uvicorn as a single process per pod and rely on horizontal scaling to add more pods. In that case, you do not need gunicorn's process management because Kubernetes handles restarts and scaling. On a bare-metal server with a fixed number of cores, gunicorn gives you more control over worker count and lifecycle.

If you are using a platform like Heroku or a PaaS that expects a single process, uvicorn alone is simpler. If you are running a traditional VM or a Docker container with multiple cores, gunicorn with uvicorn workers is a proven pattern.

There is no universal answer. The correct choice is the one that matches your application's protocol, your concurrency needs, and the operational tools you already have. Understanding what each server provides lets you make that decision with confidence.

python uvicorn vs gunicorn: Practical Usage and Code Example | RYUSLOG DEV