Filter concepts by levelShowing all levels.

Python · ASGI, WSGI, Uvicorn and Gunicorn

ASGI, WSGI, Uvicorn and Gunicorn

The interface between a Python web app and the server that runs it, and the two server processes (Uvicorn, Gunicorn) that actually implement it

  • WSGI vs. ASGIWSGI is sync-only; ASGI adds async, WebSockets, and lifespan events
  • Uvicornthe ASGI server — runs the event loop your async app needs
  • Gunicornthe process manager — supervises multiple worker processes
  • Graceful shutdownSIGTERM should finish in-flight requests before exiting

1.WSGI vs ASGI

WSGI and ASGI are the two contracts a Python web app can implement so any compliant server knows how to call it. WSGI defines one synchronous function; ASGI defines three async functions and adds support for WebSockets and background tasks.

Key features

  • WSGI (PEP 3333): app(environ, start_response) — one synchronous call per request
  • ASGI: async def app(scope, receive, send) — one async call per connection, not per request
  • WSGI has no concept of WebSockets or long-lived connections; ASGI's scope["type"] can be "http", "websocket", or "lifespan"
  • Flask and Django (pre-4.1 sync views) are WSGI; FastAPI, Starlette, and Django's async views are ASGI
  • An ASGI server (Uvicorn) can also run a WSGI app via an adapter, but a WSGI server (Gunicorn's sync worker) cannot run an ASGI app directly

WSGI vs ASGI at a glance

WSGI vs ASGI at a glance
AspectWSGIASGI
Callable shapeapp(environ, start_response)async def app(scope, receive, send)
Concurrency modelOne thread/process blocks per requestOne event loop handles many connections concurrently
WebSocketsNot supportedSupported via scope["type"] == "websocket"
Startup/shutdown hooksNot part of the speclifespan scope: startup/shutdown events
Typical serversGunicorn (sync worker), uWSGIUvicorn, Daphne, Hypercorn
Typical frameworksFlask, Django (sync views)FastAPI, Starlette, Django (async views)
One blocking call vs. three cooperating async calls

WSGI

  • app(environ, start_response)
  • Returns the full response body at once
  • Blocks the worker until the request finishes
  • No native WebSocket or streaming support

ASGI

  • async def app(scope, receive, send)
  • Sends response parts as separate messages
  • Awaits I/O instead of blocking the whole worker
  • Same interface handles HTTP, WebSocket, and lifespan
  • WSGI
    • app(environ, start_response)
    • Returns the full response body at once
    • Blocks the worker until the request finishes
    • No native WebSocket or streaming support
  • ASGI
    • async def app(scope, receive, send)
    • Sends response parts as separate messages
    • Awaits I/O instead of blocking the whole worker
    • Same interface handles HTTP, WebSocket, and lifespan

Command

python
# WSGI callable — PEP 3333
def app(environ, start_response):
    start_response(status, headers)
    return [body_bytes]

# ASGI callable — ASGI spec, three async parameters
async def app(scope, receive, send):
    message = await receive()
    await send({"type": "http.response.start", ...})
    await send({"type": "http.response.body", ...})

WSGI: one sync function, app(environ, start_response). ASGI: one async function with three parameters, app(scope, receive, send), adding WebSockets and lifespan events.

2.Uvicorn

Uvicorn is an ASGI server — it opens the network socket, accepts connections, and calls your async app(scope, receive, send) function for each one. FastAPI and Starlette apps run under Uvicorn.

Key features

  • uvicorn app:app runs the ASGI callable named app in app.py
  • --reload restarts the server on file changes — development only, never production
  • --workers N forks N worker processes, each running its own event loop
  • --host and --port control the bind address; default is 127.0.0.1:8000
  • Uvicorn can also run a WSGI app via --interface wsgi, translating WSGI calls into its own loop

Command

bash
# Development — auto-reload on file changes
uvicorn main:app --reload

# Production-shaped — bind, multiple workers, no reload
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

# Run from Python instead of the CLI
python -c "import uvicorn; uvicorn.run('main:app', host='0.0.0.0', port=8000)"

uvicorn main:app runs an ASGI app; --reload is for development, --workers N is the closest Uvicorn gets to multi-process production use on its own.

3.Gunicorn

Gunicorn ("Green Unicorn") is a WSGI process manager. It forks multiple worker processes, restarts any that crash or hang, and hands each request to one worker.

Key features

  • gunicorn main:app runs main.py's app under Gunicorn's default sync WSGI worker
  • -w N / --workers N sets the number of worker processes (default 1 — always set this explicitly)
  • -k / --worker-class picks how each worker handles requests: sync (default), gthread (threaded), or a custom class path
  • -k uvicorn.workers.UvicornWorker loads Uvicorn's ASGI-compatible worker, letting Gunicorn supervise an ASGI app like FastAPI
  • --graceful-timeout (default 30s) caps how long a worker gets to finish in-flight requests before Gunicorn force-kills it on restart or shutdown

Command

bash
# WSGI app, 4 sync worker processes
gunicorn -w 4 main:app

# WSGI app, threaded workers (each process handles several requests concurrently)
gunicorn -w 4 --worker-class gthread --threads 4 main:app

# ASGI app (e.g. FastAPI) — Gunicorn supervises, Uvicorn's worker class runs the event loop
gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:8000

Gunicorn supervises worker processes (-w N) and restarts dead ones; -k picks the worker class — sync/gthread for WSGI, uvicorn.workers.UvicornWorker to host an ASGI app.

4.Workers, worker processes, and threads

A "worker" is one OS process that a server like Gunicorn spawns to handle requests. Each worker can be single-threaded (sync), multi-threaded (gthread), or async (an event loop handling many connections without extra threads) — the worker class picks which.

Key features

  • A worker process is a full OS process spawned by Gunicorn/Uvicorn — crashing one does not take down the others
  • sync worker (Gunicorn default): one request at a time per process — a slow request blocks that whole worker
  • gthread worker: multiple threads per process, so several requests can be in-flight per worker — still limited by the GIL for CPU-bound work
  • Async worker (Uvicorn, or Gunicorn with uvicorn.workers.UvicornWorker): one event loop per process handles many concurrent connections by awaiting I/O instead of blocking a thread
  • More workers ≠ more speed once CPU cores run out — a common starting point is (2 × CPU cores) + 1 worker processes

Worker models compared

Worker models compared
ModelConcurrency unitBest forLimit
sync (default)One process, one request at a timeSimple, mostly-fast synchronous viewsOne slow request blocks the whole worker
gthreadOne process, N threadsBlocking I/O-bound work (sync DB/HTTP calls)GIL still serializes CPU-bound Python code
async (Uvicorn / UvicornWorker)One process, one event loop, many tasksAsync I/O-bound apps (FastAPI, httpx, asyncpg)A single blocking call in the loop stalls every connection on that worker
Multiple worker processesN independent processesUsing more than one CPU core; fault isolationEach process has its own memory — no shared in-process state
Three ways one worker process serves more than one request

Multiple worker processes

N independent OS processes; uses N CPU cores; one crashing does not take the others down

sync worker

one process = one request in flight at a time

gthread worker

one process = N threads = N blocking calls in flight

async worker (event loop)

one process = one loop = many awaited connections in flight

  1. Multiple worker processes — N independent OS processes; uses N CPU cores; one crashing does not take the others down
  2. sync worker — one process = one request in flight at a time
  3. gthread worker — one process = N threads = N blocking calls in flight
  4. async worker (event loop) — one process = one loop = many awaited connections in flight

Command

bash
# processes            gunicorn -w N ...
# threads per process   gunicorn --worker-class gthread --threads N ...
# async event loop       gunicorn -k uvicorn.workers.UvicornWorker ...
#                        uvicorn main:app --workers N   (N processes, each its own loop)

A worker is an OS process; sync = 1 request/process, gthread = N threads/process (GIL-limited for CPU work), async = 1 event loop/process handling many awaited connections.

5.Reverse proxies in front of a WSGI/ASGI app

A reverse proxy (Nginx, a cloud load balancer) sits between the internet and Gunicorn/Uvicorn. It terminates TLS, serves static files directly, and buffers slow clients — jobs Python's own server was never built to do efficiently.

Key features

  • TLS termination: the proxy holds the HTTPS certificate; the app server usually only speaks plain HTTP behind it
  • Static files (images, CSS, JS) are served directly by the proxy — never routed through a Python worker
  • Buffering: the proxy can absorb a slow client's request/response, freeing the worker the instant it has handed off
  • The proxy sets X-Forwarded-For / X-Forwarded-Proto so the app can see the real client IP and scheme instead of the proxy's own
  • Uvicorn/Gunicorn only trust those forwarded headers from IPs listed in forwarded_allow_ips / --proxy-headers — an unlisted source has them ignored

Command

text
Internet
   ↓  (TLS terminates here)
Nginx / load balancer   — serves static files, buffers clients, sets X-Forwarded-*
   ↓  (plain HTTP, internal network)
Gunicorn (process manager)
   ↓
Uvicorn workers (ASGI event loop)
   ↓
FastAPI / Starlette app

A reverse proxy terminates TLS, serves static files, and buffers slow clients in front of Gunicorn/Uvicorn — configure proxy_headers/forwarded_allow_ips so the app trusts only that proxy's forwarded headers.

6.Application lifecycle, graceful shutdown, and connection handling

A server's lifecycle has three phases: startup (open resources like a DB pool once), serving (handle connections), and shutdown. Graceful shutdown means finishing in-flight requests before exiting, instead of cutting them off the instant a stop signal arrives.

Key features

  • ASGI lifespan protocol: scope["type"] == "lifespan" delivers lifespan.startup then (later) lifespan.shutdown — the one place to open/close a DB pool, cache client, etc. exactly once
  • A stop signal (Ctrl+C sends SIGINT; a process manager sends SIGTERM) tells the server to stop accepting NEW connections and begin shutdown
  • Graceful shutdown = finish in-flight requests before exiting, capped by a timeout (Uvicorn: timeout_graceful_shutdown; Gunicorn: --graceful-timeout, default 30s)
  • Past that timeout, the server force-kills whatever is still running — an unbounded wait would let one stuck request block a deploy forever
  • Connection handling covers keep-alive (reusing one TCP connection for several requests) and its own timeout (Uvicorn: timeout_keep_alive, default 5s) — separate from the graceful-shutdown timeout

The three lifecycle moments and what handles each

The three lifecycle moments and what handles each
MomentSignal / eventWhat happensConfigured by
StartupASGI lifespan.startup eventApp opens DB pools, caches, background tasks — once, before the first requestFramework's startup handler (e.g. FastAPI lifespan context)
Shutdown requestedSIGTERM (process manager) or SIGINT (Ctrl+C)Server stops accepting new connections, begins draining in-flight onesUvicorn/Gunicorn's signal handler
Graceful drainIn-flight requests still runningExisting requests get to finish, up to a capped waittimeout_graceful_shutdown (Uvicorn) / --graceful-timeout (Gunicorn)
Forced stopDrain timeout exceededRemaining connections are cut off; process exitsSame timeout setting, as an upper bound
TeardownASGI lifespan.shutdown eventApp closes DB pools, flushes buffers — the mirror of startup's setupFramework's shutdown handler
From SIGTERM to process exit
withingraceful-timeouttimeout exceeded→ force-kill

Serving requests

SIGTERM received

Stop accepting new connections

Drain in-flight requests

lifespan.shutdown — close DB pool etc.

Process exits

  • Serving requests
    • leads to SIGTERM received
  • SIGTERM received
    • leads to Stop accepting new connections
  • Stop accepting new connections
    • leads to Drain in-flight requests
  • Drain in-flight requests
    • leads to lifespan.shutdown — close DB pool etc. (within graceful-timeout)
    • on error, leads to Process exits (timeout exceeded → force-kill)
  • lifespan.shutdown — close DB pool etc.
    • leads to Process exits
  • Process exits

Command

python
# ASGI lifespan protocol, at the wire level
async def app(scope, receive, send):
    if scope["type"] == "lifespan":
        while True:
            message = await receive()
            if message["type"] == "lifespan.startup":
                ...                                    # open resources
                await send({"type": "lifespan.startup.complete"})
            elif message["type"] == "lifespan.shutdown":
                ...                                    # close resources
                await send({"type": "lifespan.shutdown.complete"})
                return

Lifespan startup/shutdown events open and close resources exactly once per process; SIGTERM starts a graceful drain capped by a timeout (Gunicorn --graceful-timeout, default 30s) — configure the timeout and give the deploy tool at least that long before force-killing.

Remember

  • WSGI defines a single synchronous callable(environ, start_response); ASGI defines an async callable(scope, receive, send) and adds WebSockets and lifespan events.
  • Uvicorn runs the event loop an ASGI app needs; Gunicorn supervises multiple worker processes and restarts ones that die — they are often combined, not substitutes.
  • A reverse proxy in front of the app server handles TLS termination, static files, and buffering slow clients — the app server should not do that itself.
  • Graceful shutdown means catching SIGTERM, refusing new requests, and letting in-flight ones finish before the process exits — not dying mid-request.

Pro tip

  • gunicorn -k uvicorn.workers.UvicornWorker combines Gunicorn's process supervision with Uvicorn's ASGI event loop in one command.
  • Worker count is a starting formula (2 × CPU cores + 1 for Gunicorn's sync workers), not a fixed rule — measure under real load.
  • The ASGI lifespan protocol's startup/shutdown events are the correct place for connection-pool setup/teardown, not import-time code.

Next up

  • Monitoring and Production Operations

Sources

  • ASGI specification — asgi.readthedocs.io
  • PEP 3333 (WSGI) — peps.python.org/pep-3333
  • Uvicorn — uvicorn.org · Gunicorn — docs.gunicorn.org · checked 2026-08-22

Spotted a mistake on this page? Tell us.