ASGI vs WSGI

intermediate fastapi asgi wsgi uvicorn gunicorn

WSGI (Web Server Gateway Interface) and ASGI (Asynchronous Server Gateway Interface) are both contracts between a Python web framework and the server that runs it. In simple language: they’re the standard way a server (gunicorn, uvicorn) and a framework (Flask, FastAPI) talk to each other.

The only difference is WSGI is sync, ASGI is async.

What WSGI looks like

WSGI was defined in PEP 3333 back in 2010. The contract is dead simple: the server hands the framework an environ dict and a start_response callable, and the framework returns an iterable of bytes.

# Minimal WSGI app
def app(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello WSGI"]

One request, one function call, one response. The function is sync — it returns when the response is ready. Flask and Django are WSGI frameworks. Gunicorn is a WSGI server.

The catch: there’s no way to handle long-lived connections (websockets, server-sent events) or to suspend a handler mid-execution. The interface assumes “request in, response out, done.”

What ASGI looks like

ASGI is the async successor. The contract is an async callable that receives scope, receive, and send. Now the handler can await things and yield back to the event loop.

# Minimal ASGI app
async def app(scope, receive, send):
    await send({
        "type": "http.response.start",
        "status": 200,
        "headers": [(b"content-type", b"text/plain")],
    })
    await send({
        "type": "http.response.body",
        "body": b"Hello ASGI",
    })

Because send and receive are awaitable, the same protocol works for HTTP, websockets, and background events. FastAPI and Starlette are ASGI frameworks. Uvicorn and Hypercorn are ASGI servers.

WSGI (sync)
Frameworks: Flask, Django
Servers: Gunicorn, uWSGI
HTTP only
One worker = one request at a time
PEP 3333 (2010)
ASGI (async)
Frameworks: FastAPI, Starlette
Servers: Uvicorn, Hypercorn
HTTP + WebSockets + SSE
One worker = many concurrent
2018-present

Why FastAPI needs uvicorn (or similar)

FastAPI emits ASGI. Gunicorn (the standard WSGI server) can’t speak ASGI on its own. We need an ASGI server like uvicorn:

uvicorn main:app --host 0.0.0.0 --port 8000

For dev, uvicorn alone is fine. For prod, we want process management — multiple workers, graceful reloads, OS signal handling. That’s where gunicorn comes back in.

gunicorn + uvicorn workers

Gunicorn is a great process manager (forking, worker lifecycle, signals) but it’s WSGI by default. The trick: it supports plug-in worker classes, and uvicorn ships one. So we run:

gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker

What this means in simple language: gunicorn is the boss process that forks 4 child workers. Each child is actually a uvicorn event loop. Gunicorn handles restarts and signals, uvicorn handles the ASGI requests. Best of both worlds.

Production setup
Gunicorn (master)
↓ forks
uvicorn w1
uvicorn w2
uvicorn w3
uvicorn w4
each runs an asyncio loop

Modern FastAPI deploys are increasingly skipping gunicorn entirely and running uvicorn directly with --workers 4, since uvicorn now has its own multi-worker support. Both patterns work.

Interview cheat

If asked “why can’t we run FastAPI under gunicorn directly?” — because gunicorn’s default workers are WSGI, and FastAPI is ASGI. We either use -k uvicorn.workers.UvicornWorker or run uvicorn standalone.