22.3. Gunicorn
Table of Contents
Why Gunicorn Matters for Python Backends
Gunicorn is a production-grade application server for Python web apps. It sits between your Python code and a reverse proxy such as Nginx, and it knows how to:
- Start multiple worker processes
- Talk HTTP efficiently
- Restart workers when they crash
- Integrate cleanly with async servers like Uvicorn for ASGI apps
In a typical FastAPI or Django deployment, requests flow like this:
Client → Nginx (reverse proxy) → Gunicorn (application server) → Your app code
You usually do not expose your framework’s built in development server directly to the internet. Gunicorn is one of the tools that fills that gap for Python WSGI and ASGI applications.
WSGI vs ASGI and Gunicorn
Gunicorn was originally built for WSGI applications such as:
- Django
- Flask
- Many older or synchronous Python web frameworks
With FastAPI and other async frameworks, you have ASGI apps. Gunicorn does not directly speak ASGI, so you typically use:
gunicornas the master processuvicorn.workers.UvicornWorkeras a worker class
This combination lets Gunicorn manage processes and Uvicorn handle the ASGI protocol.
Key rule: For modern async frameworks like FastAPI, run Gunicorn with an ASGI worker class, usually uvicorn.workers.UvicornWorker. Without this you may lose async benefits or your app might not work correctly.
Basic Gunicorn Concepts
Gunicorn has a few core ideas that you should understand:
| Concept | What it is |
|---|---|
| Master process | The main Gunicorn process that starts and manages workers |
| Worker | A process that handles requests and runs your app code |
| Worker class | Implementation style, for example sync, async, Uvicorn |
| Bind address | IP and port (or Unix socket) where Gunicorn listens |
| Configuration | Command line options or a config file |
You rarely modify Gunicorn’s source code. You configure and run it around your app.
Running a Simple WSGI App with Gunicorn
Consider a minimal WSGI app in app.py:
# app.py
def application(environ, start_response):
path = environ.get("PATH_INFO", "/")
if path == "/":
status = "200 OK"
body = b"Hello from Gunicorn!"
else:
status = "404 Not Found"
body = b"Not found"
headers = [
("Content-Type", "text/plain; charset=utf-8"),
("Content-Length", str(len(body))),
]
start_response(status, headers)
return [body]Run it with Gunicorn:
gunicorn app:applicationExplanation:
appis the module name, fromapp.pyapplicationis the callable inside that module
Gunicorn now listens on 127.0.0.1:8000 by default. Visit http://127.0.0.1:8000 to see the response.
Running a FastAPI App with Gunicorn + Uvicorn
Assume you have main.py:
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from FastAPI with Gunicorn!"}You can run this with Gunicorn using Uvicorn workers:
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000What happens:
- Gunicorn starts one master process
- Master starts 4 worker processes
- Each worker runs a Uvicorn server that knows how to handle the FastAPI ASGI app
Gunicorn Command Line Options
Some of the most common options you will use:
| Option | Description | Example |
|---|---|---|
--bind or -b | Address and port or Unix socket | -b 0.0.0.0:8000 |
--workers or -w | Number of worker processes | -w 4 |
--worker-class or -k | Worker class implementation | -k uvicorn.workers.UvicornWorker |
--timeout | Seconds before killing a stuck worker | --timeout 30 |
--access-logfile | File for access logs | --access-logfile - (use stdout) |
--error-logfile | File for error logs | --error-logfile - (use stderr) |
--log-level | Logging level | --log-level info |
--reload | Auto reload on code changes, for development only | --reload |
--env | Set environment variables | --env APP_ENV=production |
--pid | Store the master process id in a file | --pid gunicorn.pid |
Use --reload only in development. For production, remove it. Auto reload uses extra resources and is less stable.
Choosing the Number of Workers
A common thumb rule for CPU bound WSGI workloads is:
$$
\text{workers} \approx 2 \times \text{CPU cores} + 1
$$
For example, on a 4 core server:
$$
\text{workers} \approx 2 \times 4 + 1 = 9
$$
For IO bound or async heavy apps such as FastAPI with async endpoints, you can often:
- Use fewer workers
- Let each async worker handle many concurrent connections
Example choice:
- 2 to 4 workers on a 4 core server
- Adjust after monitoring CPU and memory usage
Using a Configuration File
Instead of a long command, you can use a Python config file, for example gunicorn.conf.py:
# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"
timeout = 30
accesslog = "-"
errorlog = "-"
loglevel = "info"
# Optional: limit request size (byte)
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190Run your app using this config:
gunicorn main:app -c gunicorn.conf.pyWhy this is useful:
- Configuration is versioned in your repository
- Easier to review than a long shell command
- Can be different per environment, for example
gunicorn.dev.conf.pyandgunicorn.prod.conf.py
Binding to Ports and Unix Sockets
Gunicorn can listen on:
- TCP address:
gunicorn main:app -b 0.0.0.0:8000- Unix socket:
gunicorn main:app -b unix:/run/gunicorn.sockWhen using a reverse proxy such as Nginx, a Unix socket is often preferred inside the same server because it:
- Avoids TCP overhead
- Can be slightly faster
- Can be restricted via file permissions
Example Nginx upstream block for a Unix socket:
upstream app_server {
server unix:/run/gunicorn.sock fail_timeout=0;
}Worker Classes
Some typical worker classes you might see:
| Worker class | Use case |
|---|---|
sync (default) | Simple WSGI apps, mostly CPU bound |
gevent | Async IO with gevent for WSGI |
uvicorn.workers.UvicornWorker | ASGI apps such as FastAPI |
uvicorn.workers.UvicornH11Worker | Alternative Uvicorn worker using h11 |
For this course, the most important one is:
-k uvicorn.workers.UvicornWorkerYou will use this to run FastAPI or any other ASGI based web app.
Timeouts and Long Requests
Gunicorn has a timeout setting:
- Default is often 30 seconds
- If a worker does not respond in that time, it is killed and restarted
Example:
gunicorn main:app \
-k uvicorn.workers.UvicornWorker \
-w 4 \
--timeout 60
If you set timeout too low, long running requests will be killed.
If you set it too high, stuck workers will hang for a long time.
For very long operations, prefer background jobs instead of long HTTP requests.
Logging with Gunicorn
You will usually log to stdout and stderr inside a container or a modern logging setup:
gunicorn main:app \
-k uvicorn.workers.UvicornWorker \
--access-logfile - \
--error-logfile - \
--log-level infoThis lets Docker, systemd, or your logging stack collect the logs.
Example access log line might look like:
127.0.0.1 - - [10/May/2026:10:30:12 +0000] "GET / HTTP/1.1" 200 55 "-" "curl/7.79.1"You can later parse these logs to generate metrics or debug issues.
Graceful Shutdown and Signals
Gunicorn responds to Unix signals:
TERMorINTmakes it gracefully stop:- Stop accepting new requests
- Let current requests finish
- Exit cleanly
HUPcan reload configuration in some setups
In a container world, Docker sends SIGTERM when it stops a container. Gunicorn then:
- Stops taking new requests
- Lets workers finish ongoing requests up to
graceful_timeout - Exits
This is important for zero downtime deployments when combined with a reverse proxy and load balancer.
Putting It Together with Nginx
A typical production setup on one server might be:
- Nginx listening on port 80 and 443
- Terminates TLS on 443
- Proxies traffic to Gunicorn via a Unix socket
- Gunicorn runs your FastAPI app with Uvicorn workers
Simplified Nginx config snippet:
upstream app_server {
server unix:/run/gunicorn.sock;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Gunicorn command:
gunicorn main:app \
-k uvicorn.workers.UvicornWorker \
-w 4 \
-b unix:/run/gunicorn.sockThis pattern is very common:
- Reverse proxy handles TLS, static files, and load balancing
- Gunicorn handles process management and the Python app
- Your code focuses on business logic
Summary
In this chapter you saw how Gunicorn fits into a Python backend stack:
- It is an application server that runs your Python web app in production
- For FastAPI and other ASGI apps, use Gunicorn with Uvicorn workers
- You control workers, binding, and logging via command line or config files
- Gunicorn integrates naturally with reverse proxies like Nginx
You will use these patterns later when deploying FastAPI with Docker and Nginx.
Views: 7
KAHIBARO