KAHIBARO
Discord Login Register

22.3. Gunicorn

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:

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:

With FastAPI and other async frameworks, you have ASGI apps. Gunicorn does not directly speak ASGI, so you typically use:

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:

ConceptWhat it is
Master processThe main Gunicorn process that starts and manages workers
WorkerA process that handles requests and runs your app code
Worker classImplementation style, for example sync, async, Uvicorn
Bind addressIP and port (or Unix socket) where Gunicorn listens
ConfigurationCommand 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:

python
# 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:

bash
gunicorn app:application

Explanation:

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:

python
# 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:

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

What happens:

Gunicorn Command Line Options

Some of the most common options you will use:

OptionDescriptionExample
--bind or -bAddress and port or Unix socket-b 0.0.0.0:8000
--workers or -wNumber of worker processes-w 4
--worker-class or -kWorker class implementation-k uvicorn.workers.UvicornWorker
--timeoutSeconds before killing a stuck worker--timeout 30
--access-logfileFile for access logs--access-logfile - (use stdout)
--error-logfileFile for error logs--error-logfile - (use stderr)
--log-levelLogging level--log-level info
--reloadAuto reload on code changes, for development only--reload
--envSet environment variables--env APP_ENV=production
--pidStore 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:

Example choice:

Using a Configuration File

Instead of a long command, you can use a Python config file, for example gunicorn.conf.py:

python
# 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 = 8190

Run your app using this config:

bash
gunicorn main:app -c gunicorn.conf.py

Why this is useful:

Binding to Ports and Unix Sockets

Gunicorn can listen on:

  1. TCP address:
bash
   gunicorn main:app -b 0.0.0.0:8000
  1. Unix socket:
bash
   gunicorn main:app -b unix:/run/gunicorn.sock

When using a reverse proxy such as Nginx, a Unix socket is often preferred inside the same server because it:

Example Nginx upstream block for a Unix socket:

nginx
upstream app_server {
    server unix:/run/gunicorn.sock fail_timeout=0;
}

Worker Classes

Some typical worker classes you might see:

Worker classUse case
sync (default)Simple WSGI apps, mostly CPU bound
geventAsync IO with gevent for WSGI
uvicorn.workers.UvicornWorkerASGI apps such as FastAPI
uvicorn.workers.UvicornH11WorkerAlternative Uvicorn worker using h11

For this course, the most important one is:

bash
-k uvicorn.workers.UvicornWorker

You will use this to run FastAPI or any other ASGI based web app.

Timeouts and Long Requests

Gunicorn has a timeout setting:

Example:

bash
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:

bash
gunicorn main:app \
  -k uvicorn.workers.UvicornWorker \
  --access-logfile - \
  --error-logfile - \
  --log-level info

This 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:

In a container world, Docker sends SIGTERM when it stops a container. Gunicorn then:

  1. Stops taking new requests
  2. Lets workers finish ongoing requests up to graceful_timeout
  3. 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:

  1. Nginx listening on port 80 and 443
  2. Terminates TLS on 443
  3. Proxies traffic to Gunicorn via a Unix socket
  4. Gunicorn runs your FastAPI app with Uvicorn workers

Simplified Nginx config snippet:

nginx
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:

bash
gunicorn main:app \
    -k uvicorn.workers.UvicornWorker \
    -w 4 \
    -b unix:/run/gunicorn.sock

This pattern is very common:

Summary

In this chapter you saw how Gunicorn fits into a Python backend stack:

You will use these patterns later when deploying FastAPI with Docker and Nginx.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!