WSGI vs ASGI
Understanding the evolution from the synchronous WSGI standard to the modern async-first ASGI standard — the foundation that makes FastAPI possible.
WSGI — The Old Standard
WSGI (Web Server Gateway Interface) is a Python standard (PEP 3333) that defines how a web server communicates with a Python web application. Frameworks like Flask and Django are built on WSGI.
A WSGI app is just a Python callable that takes environ and start_response:
# The simplest possible WSGI application def application(environ, start_response): # environ = dict with request data (method, path, headers …) # start_response = callable to send status + headers status = '200 OK' headers = [('Content-Type', 'text/plain')] start_response(status, headers) # Must return an iterable of byte-strings return [b'Hello from WSGI!'] # How a WSGI server (e.g. Gunicorn) calls it: # result = application(environ, start_response) # for chunk in result: ← synchronous iteration # socket.write(chunk)
WSGI is strictly one request → one response. The server hands one request to the app, the app processes it (blocking), then returns one response.
import time import requests # sync HTTP library def application(environ, start_response): # 😬 This blocks the ENTIRE thread for 2 seconds! # No other request can be served during this time. data = requests.get('https://api.example.com/data') start_response('200 OK', [('Content-Type', 'text/plain')]) return [data.content] # To handle concurrency, WSGI servers spawn MULTIPLE PROCESSES/THREADS # gunicorn --workers 4 myapp:application # Each worker = 1 Python process = memory expensive
WSGI has three fundamental limitations that make it unfit for modern web applications:
| Limitation | Problem | Workaround (costly) |
|---|---|---|
| Synchronous only | Can't use async/await at all |
Thread pools / process pools |
| No WebSockets | HTTP only — no long-lived connections | Separate WebSocket server (socket.io) |
| No streaming | Must buffer full response before sending | Chunked encoding hacks |
| Thread-per-request | High memory usage under load | More servers / horizontal scaling |
ASGI — The Modern Standard
ASGI (Asynchronous Server Gateway Interface) is the modern, async-capable successor to WSGI. Instead of a callable that returns a response, an ASGI app is a coroutine that receives events and sends events.
# An ASGI app is an async callable (coroutine function) async def application(scope, receive, send): # scope = dict describing the incoming connection # receive = async callable to get incoming events # send = async callable to send outgoing events if scope['type'] == 'http': # Wait for the full request body (non-blocking!) event = await receive() # Send response start (status + headers) await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ], }) # Send response body await send({ 'type': 'http.response.body', 'body': b'Hello from ASGI!', }) # FastAPI IS just a complex ASGI application under the hood!
Unlike WSGI (HTTP only), ASGI supports multiple protocols over the same interface. The scope['type'] field tells your app what kind of connection it's dealing with.
async def application(scope, receive, send): if scope['type'] == 'http': await handle_http(scope, receive, send) elif scope['type'] == 'websocket': await handle_websocket(scope, receive, send) elif scope['type'] == 'lifespan': await handle_lifespan(scope, receive, send) # ✅ FastAPI's router does exactly this — routes to the right handler # based on scope['type'] and scope['path']
| Feature | WSGI | ASGI |
|---|---|---|
| async/await support | ✗ No | ✓ Native |
| WebSockets | ✗ No | ✓ Built-in |
| HTTP/2 streaming | ✗ Limited | ✓ Yes |
| Server-Sent Events | ✗ Hacky | ✓ Natural |
| Long-polling | ✗ Blocks thread | ✓ Non-blocking |
| Lifespan events | ✗ No | ✓ Yes |
| Memory efficiency | One thread/request | One event loop |
| Frameworks | Flask, Django | FastAPI, Starlette, Django 3.1+ |
Simulate 5 simultaneous requests hitting a WSGI vs ASGI server. Each request takes 1 second of IO (e.g. a database query).
ASGI Components
Deep dive into the three pillars of every ASGI application: Scope, Receive, and Send — and how FastAPI uses them to handle HTTP and WebSocket connections.
Scope
Scope is a Python dict that describes the incoming connection. It is set once when the connection is established and does not change during the connection's lifetime.
async def application(scope, receive, send): # scope is just a plain Python dictionary print(scope) """ { 'type': 'http', # connection type 'asgi': {'version': '3.0'}, # ASGI version info 'http_version': '1.1', # HTTP version 'method': 'GET', # HTTP method 'headers': [ # raw bytes headers (b'host', b'localhost:8000'), (b'user-agent', b'Mozilla/5.0'), ], 'path': '/users/42', # URL path 'raw_path': b'/users/42', 'query_string': b'active=true', # query params as bytes 'root_path': '', 'scheme': 'http', # 'http' or 'https' 'server': ('127.0.0.1', 8000), 'client': ('127.0.0.1', 53422), # client IP + port 'extensions': {}, } """
When scope['type'] == 'http', the scope contains everything about the incoming HTTP request — except the body (the body arrives via receive).
async def application(scope, receive, send): assert scope['type'] == 'http' method = scope['method'] # 'GET', 'POST', etc. path = scope['path'] # '/users/42' headers = dict(scope['headers']) # raw bytes dict qs = scope['query_string'] # b'page=1&limit=10' client = scope['client'] # ('192.168.1.1', 45678) # Decode headers (they're raw bytes in ASGI!) content_type = headers.get(b'content-type', b'').decode() auth_header = headers.get(b'authorization', b'').decode() # Parse query string from urllib.parse import parse_qs params = parse_qs(qs.decode()) # {'page': ['1'], 'limit': ['10']} # FastAPI does ALL of this for you automatically ✨ # You just write: async def get_user(page: int = 1): ...
'http' for HTTP connections'GET', 'POST', 'PUT', etc.'/users/42'(name_bytes, value_bytes) tuples — note: lowercase, raw bytesb'page=1&limit=10'(host, port) for the connecting client'http' or 'https'
When scope['type'] == 'websocket', the scope is similar to HTTP scope, but represents the persistent WebSocket connection. Notice there's no method field — WebSocket is bidirectional.
async def application(scope, receive, send): if scope['type'] == 'websocket': """ WebSocket scope looks like: { 'type': 'websocket', 'path': '/ws/chat', 'headers': [(b'upgrade', b'websocket'), ...], 'query_string': b'room=general', 'subprotocols': [], # WebSocket sub-protocols # Note: NO 'method' field (WebSocket has no HTTP method) } """ # Extract info path = scope['path'] # '/ws/chat' subprotocols = scope['subprotocols'] # requested sub-protocols # WebSocket lifecycle has distinct events: # 1. connect (receive) → accept or reject # 2. receive (receive) → incoming messages # 3. disconnect (receive)→ connection closed await handle_ws_connection(scope, receive, send) # FastAPI wraps this in its WebSocket class: # @app.websocket('/ws/chat') # async def chat_endpoint(websocket: WebSocket): ...
Click a request type to see what the scope dictionary looks like:
Receive — Incoming Events
Receive is an async callable (a coroutine function) that your app calls to get the next incoming event from the client. It pauses (awaits) until data actually arrives — non-blocking!
receive as a postal inbox that you check. You call await receive() to "check the inbox". If nothing is there yet, you wait — but you don't busy-loop; the event loop runs other tasks while you wait.
async def application(scope, receive, send): # Call receive() to get the next event from client event = await receive() # ↑ This AWAITS (suspends) until data arrives # Other tasks run freely during this wait! print(event) """ For HTTP request body: { 'type': 'http.request', 'body': b'{"username": "alice", "password": "secret"}', 'more_body': False # True if more chunks coming } """ # Read chunked body (for large uploads): body = b'' while True: event = await receive() body += event.get('body', b'') if not event.get('more_body', False): break # got all chunks # body is now the complete request body bytes import json data = json.loads(body) # {'username': 'alice', ...}
For HTTP connections, receive() returns two types of events:
| Event type | When it arrives | Key fields |
|---|---|---|
http.request |
When request body data is available | body (bytes), more_body (bool) |
http.disconnect |
When client disconnects (before response) | (no extra fields) |
async def application(scope, receive, send): while True: event = await receive() if event['type'] == 'http.disconnect': # Client closed connection — stop processing! print("Client disconnected, aborting.") return if event['type'] == 'http.request': chunk = event['body'] if not event.get('more_body'): break # last chunk, done reading # FastAPI handles all this automatically through Request.body() # and Request.stream() for chunked reads
WebSocket connections emit three different event types via receive():
async def handle_ws(scope, receive, send): while True: event = await receive() if event['type'] == 'websocket.connect': # Client is trying to connect — we must accept/reject await send({'type': 'websocket.accept'}) print("WebSocket connected!") elif event['type'] == 'websocket.receive': # Incoming message from client text = event.get('text') # str for text frames data = event.get('bytes') # bytes for binary frames print(f"Received: {text}") # Echo it back await send({ 'type': 'websocket.send', 'text': f'Echo: {text}', }) elif event['type'] == 'websocket.disconnect': # Client disconnected (code 1000 = normal close) code = event.get('code', 1000) print(f"Disconnected with code {code}") break
| Event type | Meaning | Key fields |
|---|---|---|
websocket.connect | Client initiated handshake | — |
websocket.receive | Client sent a message | text or bytes |
websocket.disconnect | Connection closed | code (int) |
Send — Outgoing Events
Send is an async callable that your app uses to send events back to the client. You pass it a dictionary describing the event type and data.
send is like a postal outbox. You drop a message in by calling await send({...}) and the ASGI server takes care of transmitting it to the client over the network.
async def application(scope, receive, send): # Step 1: Send the response STATUS + HEADERS first await send({ 'type': 'http.response.start', # must be first! 'status': 200, 'headers': [ (b'content-type', b'application/json'), (b'x-custom-header', b'my-value'), ], }) # Step 2: Send the response BODY (can be sent in chunks) await send({ 'type': 'http.response.body', 'body': b'{"message": "Hello World"}', 'more_body': False, # False = this is the last chunk }) # ✅ ORDER MATTERS: start must come before body! # FastAPI enforces this automatically.
http.response.start before http.response.body. Sending body first is a protocol error and will crash or behave unpredictably.Because send can be called multiple times, ASGI makes streaming trivially easy — you just call await send(body_chunk) in a loop:
import asyncio async def application(scope, receive, send): # Send headers await send({ 'type': 'http.response.start', 'status': 200, 'headers': [(b'content-type', b'text/plain')], }) # Stream body in multiple chunks ← ASGI makes this natural! for i in range(1, 6): chunk = f"Chunk {i}\n".encode() await send({ 'type': 'http.response.body', 'body': chunk, 'more_body': i < 5, # False on last chunk }) await asyncio.sleep(0.5) # simulate async data generation # FastAPI exposes this as StreamingResponse: # from fastapi.responses import StreamingResponse # async def generate(): # for i in range(5): # yield f"Chunk {i}\n" # return StreamingResponse(generate(), media_type="text/plain")
For WebSocket connections, send() supports four event types:
async def handle_ws(scope, receive, send): # 1. Accept the connection (required after websocket.connect) await send({ 'type': 'websocket.accept', 'subprotocol': None, # optional sub-protocol }) # 2. Send a text message await send({ 'type': 'websocket.send', 'text': 'Hello client!', }) # 3. Send binary data await send({ 'type': 'websocket.send', 'bytes': b'\x89PNG...', # raw bytes (image, audio, etc.) }) # 4. Close the connection await send({ 'type': 'websocket.close', 'code': 1000, # 1000 = normal close })
| Event type | When to use |
|---|---|
websocket.accept | Must be sent after receiving websocket.connect |
websocket.send (text) | Send a UTF-8 string message |
websocket.send (bytes) | Send binary data (images, protobufs, etc.) |
websocket.close | Terminate the connection cleanly |
Here's a complete ASGI app that handles both HTTP and WebSocket using all three components together:
import json async def app(scope, receive, send): """Complete ASGI application.""" if scope['type'] == 'http': await handle_http(scope, receive, send) elif scope['type'] == 'websocket': await handle_ws(scope, receive, send) async def handle_http(scope, receive, send): # Read full request body body = b'' while True: event = await receive() body += event.get('body', b'') if not event.get('more_body', False): break # Build response path = scope['path'] response = {'path': path, 'method': scope['method']} response_body = json.dumps(response).encode() # Send response await send({ 'type': 'http.response.start', 'status': 200, 'headers': [(b'content-type', b'application/json')], }) await send({ 'type': 'http.response.body', 'body': response_body, 'more_body': False, }) async def handle_ws(scope, receive, send): while True: event = await receive() if event['type'] == 'websocket.connect': await send({'type': 'websocket.accept'}) elif event['type'] == 'websocket.receive': msg = event.get('text', '') await send({'type': 'websocket.send', 'text': f'Echo: {msg}'}) elif event['type'] == 'websocket.disconnect': break # Run with Uvicorn: uvicorn myapp:app --reload
scope to route to the right endpoint, receive to parse request bodies, and send to build responses. FastAPI does all this automatically for you!Step through a real ASGI HTTP request-response lifecycle event by event:
http.response.start (headers), then http.response.bodyreceivesendscopeheaderswebsocket.connect?websocket.sendwebsocket.acceptwebsocket.receivehttp.response.start━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ WSGI vs ASGI ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ WSGI → app(environ, start_response) ← synchronous ASGI → async app(scope, receive, send) ← async ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ scope['type'] values ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 'http' → HTTP request (GET, POST, …) 'websocket' → WebSocket connection 'lifespan' → Startup / shutdown events ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ receive() event types ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ HTTP → 'http.request' (body chunks) → 'http.disconnect' (client left) WS → 'websocket.connect' (handshake) → 'websocket.receive' (message in) → 'websocket.disconnect'(closed) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ send() event types ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ HTTP → 'http.response.start' (status+headers) → 'http.response.body' (body chunks) WS → 'websocket.accept' (accept connect) → 'websocket.send' (text or bytes) → 'websocket.close' (end connection)