Python web frameworks don’t serve HTTP themselves in production — an application server does. The two most common are Gunicorn and Uvicorn. Which one you need depends on whether your app speaks WSGI or ASGI.
WSGI vs ASGI
- WSGI (Web Server Gateway Interface) — the classic, synchronous standard. One request per worker thread at a time. Flask, Django (traditional views), Pyramid, Bottle.
- ASGI (Asynchronous Server Gateway Interface) — the async successor. Supports
async/await, WebSockets, long-lived connections and many concurrent requests per worker. FastAPI, Starlette, Litestar, Django Channels, Django’s async views, Quart.
Gunicorn
A mature, pre-fork WSGI server: a master process manages several worker processes.
gunicorn app:app --bind 127.0.0.1:8000 --workers 3 --threads 2 --timeout 60
- Robust process management: restarts crashed workers, graceful reloads on
HUP, timeouts that kill stuck workers. - Worker types:
sync(default),gthread(threads),gevent/eventlet(green threads), and — importantly — Uvicorn workers for ASGI apps.
Uvicorn
A fast ASGI server built on uvloop and httptools.
uvicorn main:app --host 127.0.0.1 --port 8000 --workers 4 --proxy-headers
- Native async, WebSockets, HTTP/1.1 (and HTTP/2 via other servers like Hypercorn if needed).
--workersruns several processes; recent Uvicorn versions manage and restart workers themselves.
Gunicorn + Uvicorn workers
The long-standing production recommendation for ASGI apps combines both: Gunicorn’s process management with Uvicorn’s async workers.
gunicorn main:app -k uvicorn.workers.UvicornWorker -w 4 -b 127.0.0.1:8000
(The worker class has moved to the separate uvicorn-worker package in newer releases: -k uvicorn_worker.UvicornWorker.) With Uvicorn’s improved built-in worker management, running Uvicorn directly with --workers is now also a reasonable choice.
Which to use
| App | Server |
|---|---|
| Flask, Django (WSGI) | Gunicorn |
| FastAPI, Starlette, Litestar | Uvicorn (--workers), or Gunicorn + Uvicorn workers |
| Django with async views / Channels | Uvicorn or Daphne (ASGI) |
| Need WebSockets | ASGI (Uvicorn) |
Sizing workers and threads
Gunicorn sync workers: a classic starting point is (2 × CPU cores) + 1. Each worker is a full copy of your app in memory — on a 2 GB VPS with a 150 MB app, RAM limits you before CPU does.
Threads (--threads 4, gthread workers) let each worker handle several I/O-bound requests, with less memory than more workers.
ASGI workers: async code handles many concurrent I/O-bound requests per worker, so you typically run one worker per core. But blocking code in async def functions (synchronous database drivers, requests, time.sleep) stalls the entire event loop — use async libraries, or plain def endpoints, which FastAPI runs in a thread pool.
Measure under realistic load (e.g. with k6) and adjust.
Production settings
- Bind to
127.0.0.1(or a Unix socket) and put Nginx or Caddy in front for TLS, buffering slow clients and serving static files. - Trust proxy headers: Uvicorn
--proxy-headers --forwarded-allow-ips=127.0.0.1; FlaskProxyFix; DjangoSECURE_PROXY_SSL_HEADER. - Timeouts: Gunicorn’s
--timeoutkills workers stuck longer than N seconds; keep it above your slowest legitimate request, and move long work to a background queue. --max-requests 1000 --max-requests-jitter 100(Gunicorn) recycles workers periodically — a pragmatic guard against slow memory leaks.- Log to stdout and let systemd/journald collect it (
--access-logfile -). - Run under systemd with
Restart=always.
Guides: Django with Gunicorn and Nginx, Flask with Gunicorn, FastAPI with Uvicorn.
Never in production
flask run, manage.py runserver and uvicorn --reload are development servers — single-process, auto-reloading, not hardened. Use them only locally.