Home/Learn/FastAPI/Servers & Workers — Running FastAPI in Production

Servers & Workers — Running FastAPI in Production

Intermediate
Deployment

Production = 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.

Fork workers ≈ cores; everything per-process multiplies by N
# 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.

--proxy-headers for identity; SIGTERM → drain → teardown → exit
# 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 Aria
1

How many workers for a 4-core box running an async FastAPI app — and what changes your answer?

MediumZomato
2

After deploying behind nginx, all users share one IP and OAuth redirects go to http:// — both fixes?

MediumGroww
3

Walk through a zero-downtime deploy second by second — signals, probes, in-flight requests.

HardFlipkart

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.

Loading discussion…