Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 56

Deploying Uvicorn with Gunicorn

Recommended resources

Hello! Welcome to the final lesson in our "Testing and Production Deployment" module.

In our last session, we created a streamlined local development environment using Docker Compose. You learned to orchestrate both your FastAPI application and a PostgreSQL database with a single docker-compose.yml file, enabling features like live reloading for rapid development. The command you used, uvicorn main:app --reload, is perfect for that context.

However, a development server is not built for the demands of a production environment. It typically runs as a single process, can't fully utilize multi-core CPUs, and lacks robust process management features. This lesson bridges that gap by focusing on our learning outcome: Configure and run the application in production using Gunicorn as a process manager for Uvicorn workers.

We will explore why this specific combination is the industry standard for deploying FastAPI and how to configure it for optimal performance and stability. Coming from a Django background, you'll find the role of Gunicorn very familiar, but its interaction with Uvicorn and the ASGI world is a key distinction we'll unpack.

Why Production Requires a Different Setup

To understand the need for a production-grade server, we must first distinguish between two fundamental concepts: concurrency and parallelism.

  • Concurrency is about a single worker (like a chef) juggling multiple tasks by intelligently switching between them. When one task is waiting (e.g., for a database query to return), the worker can switch to another. This is the specialty of async programming and Uvicorn.
  • Parallelism is about executing multiple tasks simultaneously using multiple workers (like having several chefs in the kitchen). This allows you to process more orders at once by using all available resources (e.g., all CPU cores). This is the specialty of a process manager like Gunicorn.

For a robust production application, you need both.

Mastering Gunicorn and Uvicorn: The Right Way to Deploy FastAPI Applications

This article provides an excellent analogy to understand these concepts. Read the introductory section to solidify your understanding of concurrency versus parallelism.

Read the section 'Understanding the Foundation: Concurrency vs Parallelism'. Focus on the chef analogy and the characteristics tables, which clearly outline the differences.


The Production Stack: Gunicorn + Uvicorn

In a production environment, Uvicorn and Gunicorn form a powerful team.

  • Gunicorn (Green Unicorn) acts as the process manager. Think of it as the general manager of your restaurant. It starts multiple worker processes, monitors their health, restarts them if they crash, and distributes incoming requests (load balancing) among them. This provides parallelism and resilience.
  • Uvicorn acts as the ASGI server running inside each worker process. It's the lightning-fast chef who can handle thousands of concurrent I/O-bound requests within its own kitchen (process) thanks to its async capabilities.

The following diagram illustrates this architecture. An incoming request first hits a reverse proxy (like Nginx, which we'll cover next lesson), which passes it to Gunicorn. Gunicorn then routes it to one of its available Uvicorn workers, which finally communicates with your FastAPI application.

FastAPI Production Deployment Architecture
This diagram shows the flow of requests in a production environment. Gunicorn manages multiple Uvicorn worker processes, each running an instance of your FastAPI application. This setup allows the application to handle requests in parallel across multiple CPU cores, while each worker handles many concurrent connections asynchronously.

The Correct Configuration

The key is to let Gunicorn manage the processes and use a special Uvicorn worker class. The command looks like this:

gunicorn my_app.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

Let's break this down:

  • gunicorn ...: We are running the Gunicorn server.
  • my_app.main:app: This points to the app object inside the my_app/main.py file.
  • --workers 4 (or -w 4): Gunicorn will start 4 worker processes. This is how you achieve parallelism.
  • --worker-class uvicorn.workers.UvicornWorker (or -k uvicorn.workers.UvicornWorker): This is the most crucial part. It tells Gunicorn to use Uvicorn to run the code inside each worker process. This gives each worker its async superpowers.
  • --bind 0.0.0.0:8000: The server will listen on port 8000 on all available network interfaces inside the container.

This approach is officially recommended by the FastAPI documentation. To understand why it's so important to get this right, let's explore both the correct pattern and a common, costly mistake.

Mastering Gunicorn and Uvicorn: The Right Way to Deploy FastAPI Applications

The same article we looked at earlier has a fantastic section explaining exactly how to structure this command and, just as importantly, what not to do.

Please read three sections from this article: 'The Common Mistake: Worker Multiplication': Understand why having uvicorn.run(workers=...) in your application code is a disaster when Gunicorn is also managing workers. 'The Correct Pattern: Unified Architecture': Study the CMD example in the Dockerfile and the breakdown of each parameter. Note that the application code itself contains no server startup logic. 'Performance Comparison: Right vs Wrong': Skim this section to see the dramatic performance difference between the correct and incorrect setups.


Tuning Performance: How Many Workers?

A common question is: "How many workers should I run?" The answer depends on your server's resources and your application's workload.

A widely cited rule of thumb, which comes from the world of synchronous (WSGI) frameworks like Django, is (2 * number_of_cpu_cores) + 1.

However, for async workers that handle I/O very efficiently, a simpler starting point is often recommended:
workers = number_of_cpu_cores

This minimizes the overhead from context-switching between processes, letting each Uvicorn worker make full use of its assigned CPU core to handle thousands of concurrent connections. You might increase this to (2 * cores) for very I/O-heavy applications, but starting with one worker per core is a safe and performant baseline.

Let's watch a quick video that benchmarks different setups and demonstrates the effect of adding workers.

Best Way to Deploy FastAPI in Production

This video by Code Collider provides a practical performance test, comparing Uvicorn alone against Gunicorn with different numbers of Uvicorn workers.

Watch from 01:39 to 03:15. Pay attention to how performance (Requests Per Second) changes as the configuration moves from plain Uvicorn to Gunicorn with 1 worker, and then to Gunicorn with 2 workers (on a 2-CPU machine). This shows the practical benefit of this architecture.


A Professional Configuration File

While command-line arguments are fine, managing them can become cumbersome. A more professional approach is to use a configuration file, typically gunicorn_config.py. This keeps your deployment configuration clean, version-controlled, and separate from your application logic.

This file allows you to set not only the number of workers but also other crucial production settings.

FastAPI production deployment best practices

This article from Render's blog introduces several advanced Gunicorn settings and shows how to place them in a gunicorn_config.py file.

Read the section 'Worker configuration and resource management' and study the example gunicorn_config.py file. Focus on what these settings do: workers: The number of worker processes. worker_class: Set to uvicorn.workers.UvicornWorker. max_requests and max_requests_jitter: A brilliant feature to automatically restart workers periodically, preventing slow memory leaks. graceful_timeout: Allows workers to finish handling a request before shutting down during a new deployment.

With a gunicorn_config.py file in your project root, your startup command becomes much cleaner:

gunicorn -c gunicorn_config.py my_app.main:app

Updating Docker for Production

Finally, let's tie this back to the Docker setup from our previous lesson. For production, you'll want to modify your Dockerfile's CMD or your docker-compose.yml's command to use the new Gunicorn startup command.

Here’s how you might adapt the CMD in your Dockerfile for a production-ready image:




# Assuming your Dockerfile has already copied the code and installed dependencies...




# Expose the port Gunicorn will run on
EXPOSE 8000




# Copy the Gunicorn config file into the container
COPY ./gunicorn_config.py /app/gunicorn_config.py




# Set the command to run Gunicorn
CMD ["gunicorn", "-c", "/app/gunicorn_config.py", "my_app.main:app"]

Notice that we are no longer using --reload, and we've replaced the direct uvicorn call with our robust gunicorn command.


Conclusion

You have now learned the industry-standard method for deploying FastAPI applications. By combining Gunicorn's process management with Uvicorn's asynchronous speed, you get a scalable, resilient, and highly performant server setup.

Key Takeaways:

  • Production requires both parallelism and concurrency. Gunicorn provides parallelism by managing multiple worker processes, while Uvicorn provides concurrency within each worker using an async event loop.
  • The correct way to combine them is by running gunicorn with the --worker-class uvicorn.workers.UvicornWorker.
  • A good starting point for the number of workers is one per available CPU core.
  • Never configure workers in both Gunicorn and your application code (uvicorn.run(workers=...)), as this leads to process multiplication and severe performance degradation.
  • Using a gunicorn_config.py file is a professional best practice for managing your deployment settings.

Preview of the Next Lesson:

Our application is now running on a powerful server setup inside a container. But how do we expose it securely to the outside world? How do we handle things like SSL/TLS encryption (HTTPS) or route traffic to multiple application instances? The final piece of our production puzzle is a reverse proxy. In the next lesson, we will explain the role of a reverse proxy like Nginx in a production deployment for handling SSL and load balancing.