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).
  • --workers runs 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; Flask ProxyFix; Django SECURE_PROXY_SSL_HEADER.
  • Timeouts: Gunicorn’s --timeout kills 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.

  • Python
  • Gunicorn
  • Uvicorn