FastAPI Mastery
Topic 3 of 22 — ASGI Fundamentals
Overall progress
13.6%
Topic 3 · Section 3.1

WSGI vs ASGI

Understanding the evolution from the synchronous WSGI standard to the modern async-first ASGI standard — the foundation that makes FastAPI possible.

3.1.1

WSGI — The Old Standard

📜
What is WSGI?

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.

🍽 Real-world analogy
Think of WSGI as a diner with one waiter. A customer (HTTP request) comes in, the waiter takes the order, goes to the kitchen, waits, brings the food back — and only then is ready for the next customer. One at a time, fully blocking.

A WSGI app is just a Python callable that takes environ and start_response:

python — minimal WSGI app
# 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 was designed in 2003 when async Python didn't exist. It was a huge step forward — it let one server run many frameworks — but it was fundamentally synchronous.
🔄
Request-Response Model

WSGI is strictly one request → one response. The server hands one request to the app, the app processes it (blocking), then returns one response.

Browser
HTTP Request
WSGI Server
app(environ, start_response)
HTTP Response
Browser
python — WSGI blocking IO problem
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 Limitations

WSGI has three fundamental limitations that make it unfit for modern web applications:

LimitationProblemWorkaround (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
If your app calls a database, makes HTTP requests, or reads files — your WSGI threads spend most of their time waiting. ASGI was designed to eliminate this waste.
3.1.2

ASGI — The Modern Standard

Async Lifecycle

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.

🍽 Real-world analogy
ASGI is like a restaurant with a single waiter who uses a pager system. The waiter takes order 1, pings the kitchen, immediately takes order 2, pings kitchen, then delivers food to whichever table is ready — all without blocking on any single task.
python — minimal ASGI app
# 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!
💡
Notice the three arguments: scope, receive, send. These are the core of the ASGI interface — we'll explore each in detail in Section 3.2.
🌐
Protocol Support

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.

http Standard HTTP/1.1 or HTTP/2 request-response cycles
websocket Long-lived bidirectional WebSocket connections (chat, live data)
lifespan Startup and shutdown events for the application process
python — ASGI multi-protocol handling
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']
FeatureWSGIASGI
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 efficiencyOne thread/requestOne event loop
FrameworksFlask, DjangoFastAPI, Starlette, Django 3.1+
🎮
Interactive: Concurrency Simulation

Simulate 5 simultaneous requests hitting a WSGI vs ASGI server. Each request takes 1 second of IO (e.g. a database query).

Concurrency Simulator
→ Click a button to simulate 5 concurrent requests...
🧠
Quick Quiz
Which Python web server standard natively supports WebSockets?
A) WSGI
B) ASGI
C) Both
D) Neither, you need a separate library
Topic 3 · Section 3.2

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.

3.2.1

Scope

📋
What is 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.

📋 Real-world analogy
Scope is like the header on a form — it tells you who filled it out, what type of request it is, which page they came from. It's set up-front and doesn't change as the conversation continues.
python — what scope looks like
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': {},
    }
    """
🌐
HTTP Scope

When scope['type'] == 'http', the scope contains everything about the incoming HTTP request — except the body (the body arrives via receive).

python — reading HTTP scope fields
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): ...
typeAlways 'http' for HTTP connections
methodHTTP verb: 'GET', 'POST', 'PUT', etc.
pathURL path string: '/users/42'
headersList of (name_bytes, value_bytes) tuples — note: lowercase, raw bytes
query_stringRaw bytes: b'page=1&limit=10'
clientTuple of (host, port) for the connecting client
scheme'http' or 'https'
🔌
WebSocket Scope

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.

python — WebSocket scope
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): ...
ℹ️
Key difference: HTTP scope lives for one request-response cycle. WebSocket scope persists for the entire lifetime of the connection — potentially minutes or hours.
🔍
Interactive: Scope Explorer

Click a request type to see what the scope dictionary looks like:

scope dict

            
3.2.2

Receive — Incoming Events

📥
What is Receive?

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!

📥 Real-world analogy
Think of 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.
python — receive in action
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', ...}
📨
HTTP Receive Events

For HTTP connections, receive() returns two types of events:

Event typeWhen it arrivesKey fields
http.request When request body data is available body (bytes), more_body (bool)
http.disconnect When client disconnects (before response) (no extra fields)
python — handling disconnect mid-stream
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 Receive Events

WebSocket connections emit three different event types via receive():

python — WebSocket receive loop
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 typeMeaningKey fields
websocket.connectClient initiated handshake
websocket.receiveClient sent a messagetext or bytes
websocket.disconnectConnection closedcode (int)
3.2.3

Send — Outgoing Events

📤
What is Send?

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.

📤 Real-world analogy
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.
python — complete HTTP response via send
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.
⚠️
Always send http.response.start before http.response.body. Sending body first is a protocol error and will crash or behave unpredictably.
🌊
Streaming Responses

Because send can be called multiple times, ASGI makes streaming trivially easy — you just call await send(body_chunk) in a loop:

python — streaming chunks via ASGI send
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")
🔌
WebSocket Send Events

For WebSocket connections, send() supports four event types:

python — all WebSocket send events
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 typeWhen to use
websocket.acceptMust 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.closeTerminate the connection cleanly
🏗️
Putting it All Together — Complete ASGI App

Here's a complete ASGI app that handles both HTTP and WebSocket using all three components together:

python — complete ASGI app (scope + receive + send)
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
💡
This is essentially what FastAPI is — a very sophisticated ASGI app that uses scope to route to the right endpoint, receive to parse request bodies, and send to build responses. FastAPI does all this automatically for you!
🎮
Interactive: ASGI Lifecycle Simulator

Step through a real ASGI HTTP request-response lifecycle event by event:

ASGI HTTP Lifecycle
→ Press "Next Event" to step through the ASGI lifecycle...
🧠
Section Quiz — Test Your Knowledge
Q1: In an ASGI app, what is the correct order to send an HTTP response?
A) Send body first, then headers
B) Send http.response.start (headers), then http.response.body
C) Call receive() first, then send()
D) Order doesn't matter in ASGI
Q2: Which ASGI component describes the incoming connection type and is set once at connection time?
A) receive
B) send
C) scope
D) headers
Q3: What event must you send after receiving websocket.connect?
A) websocket.send
B) websocket.accept
C) websocket.receive
D) http.response.start
📌
Topic 3 Cheat Sheet
ASGI quick reference
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  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)