Uvicorn vs Gunicorn + Uvicorn Workers
FastAPI is an ASGI framework, so it requires an ASGI server. Two common approaches:
| Approach | Command | Use Case |
|---|---|---|
| Uvicorn directly | uvicorn app.main:app | Simple deployments, containers, single-process |
| Gunicorn + Uvicorn workers | gunicorn -k uvicorn.workers.UvicornWorker | Multi-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
| Option | Description |
|---|---|
--host | Bind address (0.0.0.0 for all interfaces) |
--port | Port number |
--workers | Number of worker processes |
--loop | Event loop (uvloop is fastest on Linux) |
--http | HTTP parser (httptools is fastest) |
--log-level | Logging 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 Class | ASGI | When to Use |
|---|---|---|
uvicorn.workers.UvicornWorker | Yes | Standard FastAPI apps |
uvicorn.workers.UvicornH11Worker | Yes | Pure-Python HTTP (no httptools) |
uvicorn.workers.UvicornWorker (uvloop) | Yes | Recommended - 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 -
TERMsignal: master stops accepting connections, waits for in-flight requests (up totimeout), then kills workers
Signal Handling
| Signal | Gunicorn | Uvicorn |
|---|---|---|
SIGHUP | Reload config, restart workers | N/A |
SIGTERM | Graceful shutdown | Graceful shutdown |
SIGUSR2 | Spawn new master (zero-downtime deploy) | N/A |
SIGTTIN | Increase workers by 1 | N/A |
SIGTTOU | Decrease workers by 1 | N/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
| Factor | Uvicorn Direct | Gunicorn + Uvicorn |
|---|---|---|
| Process management | Manual | Automatic (master/worker) |
| Zero-downtime deploy | Manual (load balancer) | Built-in (USR2 signal) |
| Worker count config | --workers N | --workers N + dynamic signals |
| Docker / K8s | ✅ Simpler, single process | Optional (K8s handles restart) |
| Traditional VPS | Less common | ✅ Recommended |
| Memory overhead | Lower | Slightly 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
--reloadin production - it adds overhead and bypasses signal handling - Set
--workersto2 * CPU_COUNT + 1for most workloads; tune with--max-requeststo prevent memory leaks - Configure structured JSON logging for production log aggregation (ELK, Datadog, Grafana Loki)
- Always expose a
/healthendpoint for load balancer and orchestrator health checks