WSGI vs. ASGI Architecture & Application Server Lifecycles
Python web applications interface with production web servers (Nginx, Apache) through standardized Gateway Interfaces: WSGI (Web Server Gateway Interface / PEP 3333) for synchronous frameworks (Flask, Django) and ASGI (Asynchronous Server Gateway Interface) for asynchronous frameworks (FastAPI, Starlette, Django Channels).
This chapter details WSGI environment callables, ASGI scope/receive/send onion architectures, Gunicorn pre-fork worker lifecycles, and Uvicorn ASGI event loops.
1. WSGI Architecture (PEP 3333)
WSGI (PEP 3333) defines a synchronous single-call interface between a web server (Gunicorn, uWSGI) and a Python application framework:
WSGI Synchronous Execution Flow:
[ Nginx (Reverse Proxy) ] ββ> [ Gunicorn WSGI Server ]
|
v Calls: application(environ, start_response)
[ WSGI Application (Flask / Django) ]
|
v (Synchronous Execution)
[ Returns iterable of bytes ]The WSGI Callable Signature:
A WSGI application is a single callable (function or object with __call__) accepting two positional parameters:
# Raw WSGI Application Callable
def application(environ: dict, start_response: callable):
# 1. 'environ': Dictionary containing HTTP headers, PATH_INFO, QUERY_STRING
path = environ.get("PATH_INFO", "/")
# 2. 'start_response': Callback setting HTTP status and response headers
status = "200 OK"
headers = [("Content-Type", "text/plain; charset=utf-8")]
start_response(status, headers)
# 3. Return iterable of body bytes
return [b"Hello, WSGI World!"]WSGI Limitation:
WSGI is strictly synchronous and single-request-per-worker. It cannot support WebSockets, HTTP/2 Server Push, or Long-Polling without blocking worker processes.
2. ASGI Architecture (PEP 3090)
ASGI replaces WSGIβs synchronous callable with an asynchronous 3-argument callable: app(scope, receive, send).
ASGI Asynchronous Onion Architecture:
[ Client Socket ] ββ> [ Uvicorn ASGI Server ]
|
v Calls: await app(scope, receive, send)
[ ASGI Application (FastAPI / Starlette) ]
|
βββ scope: Connection metadata (type="http" or "websocket")
βββ receive(): awaitable to read incoming bytes / frames
βββ send(): awaitable to push response headers / frames# Raw ASGI 3.0 Application Callable
async def application(scope: dict, receive: callable, send: callable):
if scope["type"] == "http":
# Wait for HTTP request body
request_event = await receive()
# Send response headers
await send({
"type": "http.response.start",
"status": 200,
"headers": [[b"content-type", b"text/plain"]],
})
# Send response body payload
await send({
"type": "http.response.body",
"body": b"Hello, ASGI World!",
})
elif scope["type"] == "websocket":
# Handle persistent WebSocket frames!
pass3. Production Server Architecture: Gunicorn & Uvicorn
In production, Python applications are run behind a process manager and reverse proxy:
Production Deployment Hierarchy:
[ Client Requests ] ββ> [ Nginx (Port 80/443: SSL Termination & Static Files) ]
|
v (Unix Domain Socket / TCP)
[ Gunicorn Master Process (PID 1) ]
βββ Worker Process 1 (Uvicorn Worker - Async Event Loop) ββ> [ FastAPI App ]
βββ Worker Process 2 (Uvicorn Worker - Async Event Loop) ββ> [ FastAPI App ]
βββ Worker Process N (Uvicorn Worker - Async Event Loop) ββ> [ FastAPI App ]- Gunicorn Pre-Fork Model: Master process manages worker process lifecycles, monitoring PIDs and restarting workers if memory limits or timeouts occur.
UvicornWorker: Runs Uvicornβs high-speeduvloopasync event loop inside each Gunicorn worker process, providing multi-process multi-core async processing!
# Production Gunicorn Command with Uvicorn Worker Class
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000