Running with Uvicorn & Gunicorn · learncode.live

Uvicorn vs Gunicorn + Uvicorn Workers

FastAPI is an ASGI framework, so it requires an ASGI server. Two common approaches:

ApproachCommandUse Case
Uvicorn directlyuvicorn app.main:appSimple deployments, containers, single-process
Gunicorn + Uvicorn workersgunicorn -k uvicorn.workers.UvicornWorkerMulti-process, production servers, process management

Uvicorn is a lightning-fast ASGI server written in Rust-backed uvloop. Gunicorn is a mature WSGI HTTP server that provides process management (worker spawning, restart, signals) - and can delegate to Uvicorn workers to handle ASGI apps.

Running Uvicorn Directly

Suitable for containers (Docker, Kubernetes) where the orchestrator manages processes:

pip install uvicorn

# Basic
uvicorn app.main:app --host 0.0.0.0 --port 8000

# Production settings
uvicorn app.main:app \
  --host 0.0.0.0 \
  --port 8000 \
  --workers 4 \
  --loop uvloop \
  --http httptools \
  --log-level info

Key Uvicorn Options

OptionDescription
--hostBind address (0.0.0.0 for all interfaces)
--portPort number
--workersNumber of worker processes
--loopEvent loop (uvloop is fastest on Linux)
--httpHTTP parser (httptools is fastest)
--log-levelLogging verbosity

--reload is for Development Only

# ❌ Never use in production
uvicorn app.main:app --reload

# ✅ Production - no reload
uvicorn app.main:app --workers 4

--reload watches the filesystem for changes and restarts the server. This adds overhead, exposes the filesystem, and prevents proper signal handling. Container orchestrators (Kubernetes, Docker Swarm) handle restarts via health checks.

Running with Gunicorn + Uvicorn Workers

Gunicorn brings battle-tested process management to ASGI apps:

pip install gunicorn uvicorn

gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --workers 4 \
  --timeout 120 \
  --keep-alive 5 \
  --max-requests 1000 \
  --max-requests-jitter 50 \
  --log-level info \
  --access-logfile -

Gunicorn Worker Types

Worker ClassASGIWhen to Use
uvicorn.workers.UvicornWorkerYesStandard FastAPI apps
uvicorn.workers.UvicornH11WorkerYesPure-Python HTTP (no httptools)
uvicorn.workers.UvicornWorker (uvloop)YesRecommended - fastest on Linux
# config/gunicorn.conf.py
import multiprocessing

bind = "0.0.0.0:8000"
worker_class = "uvicorn.workers.UvicornWorker"
workers = multiprocessing.cpu_count() * 2 + 1
timeout = 120
keepalive = 5
max_requests = 1000
max_requests_jitter = 50
graceful_timeout = 30
log_level = "info"
accesslog = "-"
errorlog = "-"

Run with a config file:

gunicorn app.main:app -c config/gunicorn.conf.py

Gunicorn Process Management

┌─────────────────────────────────────────────┐
│           Gunicorn Master (PID 1)            │
│          ┌───────────┬───────────┐          │
│          │   Arbiter  │  Listener │          │
│          └─────┬─────┴─────┬─────┘          │
│                │           │                 │
│   ┌────────────┼───────────┼────────────┐   │
│   │      ┌─────┴─────┐ ┌──┴──────┐     │   │
│   │      │ Worker 1  │ │ Worker 2│ ... │   │
│   │      │ Uvicorn   │ │ Uvicorn │     │   │
│   │      └───────────┘ └─────────┘     │   │
│   └────────────────────────────────────┘   │
└─────────────────────────────────────────────┘
  • Master process - manages workers, handles signals (HUP, TERM, USR2)
  • Worker processes - each runs a Uvicorn ASGI server instance
  • Auto-restart - if a worker crashes, the master spawns a replacement
  • Graceful shutdown - TERM signal: master stops accepting connections, waits for in-flight requests (up to timeout), then kills workers

Signal Handling

SignalGunicornUvicorn
SIGHUPReload config, restart workersN/A
SIGTERMGraceful shutdownGraceful shutdown
SIGUSR2Spawn new master (zero-downtime deploy)N/A
SIGTTINIncrease workers by 1N/A
SIGTTOUDecrease workers by 1N/A

Zero-Downtime Reload with Gunicorn

# Graceful restart with new code
kill -HUP <gunicorn-master-pid>

# Or spawn a new master alongside the old one
kill -USR2 <gunicorn-master-pid>

Logging Configuration

Uvicorn Logging

# logging_config.py
import logging

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "default": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        },
        "access": {
            "format": "%(asctime)s [%(levelname)s] %(client_addr)s - %(request_line)s %(status_code)s",
        },
    },
    "handlers": {
        "default": {
            "formatter": "default",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stderr",
        },
        "access": {
            "formatter": "access",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
        },
    },
    "loggers": {
        "uvicorn": {"handlers": ["default"], "level": "INFO"},
        "uvicorn.error": {"handlers": ["default"], "level": "INFO"},
        "uvicorn.access": {"handlers": ["access"], "level": "INFO"},
    },
}

Pass it when creating the FastAPI app:

import uvicorn

if __name__ == "__main__":
    uvicorn.run("app.main:app", host="0.0.0.0", port=8000, log_config=LOGGING_CONFIG)

Structured JSON Logging

pip install python-json-logger
from pythonjsonlogger import jsonlogger

LOGGING_CONFIG["formatters"]["default"] = {
    "()": jsonlogger.JsonFormatter,
    "format": "%(asctime)s %(levelname)s %(name)s %(message)s",
}

Choosing Between Uvicorn and Gunicorn

FactorUvicorn DirectGunicorn + Uvicorn
Process managementManualAutomatic (master/worker)
Zero-downtime deployManual (load balancer)Built-in (USR2 signal)
Worker count config--workers N--workers N + dynamic signals
Docker / K8s✅ Simpler, single processOptional (K8s handles restart)
Traditional VPSLess common✅ Recommended
Memory overheadLowerSlightly higher (master process)

On a typical VPS or bare-metal server, Gunicorn + UvicornWorkers is the standard choice. In containers where the orchestrator manages processes, running Uvicorn directly is simpler and equally robust.

Health Check Endpoint

from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()


@app.get("/health")
def health_check():
    return JSONResponse(
        content={"status": "healthy"},
        status_code=200,
        headers={"Cache-Control": "no-store"},
    )

Configure Gunicorn to use it:

# gunicorn.conf.py
from app.main import app

# Or use a readiness endpoint approach with
# gunicorn --check-config ...

For Kubernetes, add liveness and readiness probes pointing to /health.

Key Takeaways

  • Use Uvicorn directly (--workers N) in Docker/Kubernetes where the orchestrator handles process management
  • Use Gunicorn + UvicornWorker on bare-metal/VPS for automatic worker management, signals, and zero-downtime reloads
  • Never use --reload in production - it adds overhead and bypasses signal handling
  • Set --workers to 2 * CPU_COUNT + 1 for most workloads; tune with --max-requests to prevent memory leaks
  • Configure structured JSON logging for production log aggregation (ELK, Datadog, Grafana Loki)
  • Always expose a /health endpoint for load balancer and orchestrator health checks
Courses