Servers & Workers — Running FastAPI in Production
IntermediateProduction = multiple Uvicorn worker processes behind a reverse proxy: workers ≈ cores for async apps, --proxy-headers so the app sees real client IPs and HTTPS, and graceful shutdown so deploys drop zero requests.
Overview
fastapi dev is a development tool; production runs Uvicorn with multiple worker processes — separate OS processes (the multiprocessing lesson: one GIL each), sharing nothing, load-balanced on one port. Worker count starts at CPU core count for async apps (each worker's event loop can saturate a core) — the old (2 × cores) + 1 heuristic belongs to sync/threaded stacks. In front sits a reverse proxy (nginx, or your platform's — Fly proxy, an ALB) terminating TLS, and that creates the identity problem --proxy-headers solves: without it every client appears to be 127.0.0.1 and url_for generates http:// links on an https:// site. Graceful shutdown ties it together: on SIGTERM workers finish in-flight requests, run lifespan teardown, then exit — which, combined with readiness probes, is what makes rolling deploys drop zero requests. Managed platforms (Fly, Render, Railway) run this exact recipe for you; you still choose the worker count and the health endpoints.
Workers, Sizing & the Process Model
One command turns one process into a fleet. Remember what per-worker means (lifespan, memory, in-process caches × N) and size by measurement, not folklore.
# Development:
fastapi dev main.py # 1 process, auto-reload — dev ONLY
# Production:
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
# What --workers 4 actually does:
# master process (PID 1) supervises 4 forked workers
# each worker: own interpreter, own GIL, own event loop, own lifespan run
# the OS load-balances the port's connections across them
# one worker crashes → master replaces it; the other 3 keep serving
# Sizing:
# async-heavy app (I/O-bound): workers = cores (loop saturates a core)
# lots of blocking def routes: more workers help, but fix the code first
# memory check: 4 workers × (app + model + pools) must fit RAM —
# the 2GB ML model from the lifespan chapter × 4 = 8GB. Plan for it.
# then MEASURE under representative load and adjust. No formula survives
# contact with a real workload.
# Per-worker consequences you already met:
# lifespan runs 4× (chapter: Lifespan) · in-process caches drift 4 ways
# (chapter: Caching) · WS ConnectionManagers are per-worker too —
# even ONE machine with 4 workers needs the Redis pub/sub bridge
# (chapter: WebSockets)
# Gunicorn as the process manager (the classic recipe, still common):
gunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 \
--graceful-timeout 30 --bind 0.0.0.0:8000
# Uvicorn's own --workers is fine today; gunicorn adds mature process
# management (worker recycling via max-requests, detailed timeouts).Behind the Proxy + Graceful Shutdown
TLS terminates at the proxy, so the app must trust forwarded headers to know real IPs and scheme. SIGTERM handling + readiness probes = deploys users never notice.
# The proxy chain: client ──HTTPS──▶ nginx/Fly-proxy ──HTTP──▶ uvicorn
uvicorn app.main:app --workers 4 \
--proxy-headers --forwarded-allow-ips="10.0.0.0/8"
# --proxy-headers: trust X-Forwarded-For / X-Forwarded-Proto
# without it: request.client.host == the proxy, always;
# request.url.scheme == "http" → redirects & url_for build http:// links
# --forwarded-allow-ips: ONLY trust those headers from YOUR proxy's range —
# trusting everyone lets any client spoof its IP (Headers chapter's warning)
# nginx essentials for the FastAPI upstream:
# location / {
# proxy_pass http://127.0.0.1:8000;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_http_version 1.1; # keep-alive to the app
# proxy_read_timeout 75s; # raise for SSE/WebSockets!
# client_max_body_size 10m; # the upload cap (Files chapter)
# }
# ── Graceful shutdown: the zero-dropped-request deploy ──
# 1. platform sends SIGTERM to old instance
# 2. readiness probe flips → LB stops sending NEW requests
# 3. workers finish in-flight requests (up to --timeout-graceful-shutdown)
# 4. lifespan teardown runs (close pools, flush)
# 5. exit; new instance (already ready) has the traffic
uvicorn app.main:app --workers 4 --timeout-graceful-shutdown 30
# The pieces you built earlier make this work: lifespan teardown +
# /health/ready. Long-lived connections (SSE, WS) need clients that
# reconnect — the graceful window won't outlast a 2-hour socket.Key Points to Remember
- 1Production = Uvicorn --workers N (≈ cores for async apps) behind a reverse proxy
- 2Everything per-worker multiplies: lifespan, memory, caches, WS managers
- 3--proxy-headers + --forwarded-allow-ips restore real client IP and https scheme
- 4Graceful shutdown (drain → teardown → exit) + readiness probes = zero-drop deploys
Interview Questions
Sign in to ask AriaHow many workers for a 4-core box running an async FastAPI app — and what changes your answer?
After deploying behind nginx, all users share one IP and OAuth redirects go to http:// — both fixes?
Walk through a zero-downtime deploy second by second — signals, probes, in-flight requests.
Ask Aria about Servers & Workers — Running FastAPI in Production
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.