WSGI vs ASGI

RoadmapsPython Backend

Scenario

Your Flask app handles 10 requests per second just fine. You launch a marketing campaign, traffic spikes to 100 requests per second, and suddenly the server stops responding entirely. CPU usage is nearly zero. The database is just slow, and your WSGI workers are sitting there completely blocked, unable to accept new connections.

Mental model

WSGI is a bank with one teller per customer; if a customer is filling out a form, the teller just waits. ASGI is a restaurant with one waiter for the whole floor; while one table reads the menu (waiting on I/O), the waiter takes orders from another table.

WSGI is strictly synchronous. It assigns a worker to a request and that worker is trapped until the request is completely finished, even if it's just waiting for a database. ASGI is built around Python's asyncio event loop, allowing a single worker to juggle thousands of requests by yielding control during I/O.

Deep dive

For decades, Python web apps (like Flask or Django) communicated with web servers using WSGI (Web Server Gateway Interface). It is purely synchronous. Every time a request comes in, a worker (a thread or process) is assigned to it. That worker is trapped until it returns a response.

WSGI assigns one dedicated worker per request from start to finish.

The WSGI model breaks under heavy I/O. If your app asks the database for information and the query takes 2 seconds, that WSGI worker is frozen for 2 seconds doing absolutely nothing. If you have 4 workers, 4 slow requests will completely block your server from accepting a 5th request.

I/O bound operations freeze WSGI workers, killing concurrency.

ASGI (Asynchronous Server Gateway Interface) was created to solve this. It's built around Python's `asyncio` event loop. When an ASGI app hits an I/O boundary (like a slow database query), it `await`s the result. The event loop instantly pauses that request and assigns the worker to handle the next incoming request.

ASGI uses `await` to yield control during I/O, allowing one worker to juggle thousands of requests.

This is why modern async frameworks like FastAPI require an ASGI server (like Uvicorn) to run. If you try to run FastAPI on a pure WSGI server, it either crashes or requires a compatibility wrapper that defeats the purpose of async.

FastAPI is an ASGI framework and requires an ASGI server.

Code examples

A minimal WSGI application

def wsgi_app(environ, start_response):
    # Synchronous: blocks until complete
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    return [b"Hello World"]

# Run with: gunicorn app:wsgi_app

WSGI apps are simple callables that take the environment and a callback. They return an iterable of bytes. There is no way to yield control here if the code needs to wait for a network call.

A minimal ASGI application

async def asgi_app(scope, receive, send):
    # Asynchronous: can await I/O
    assert scope['type'] == 'http'
    
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ],
    })
    
    await send({
        'type': 'http.response.body',
        'body': b'Hello World',
    })

# Run with: uvicorn app:asgi_app

ASGI apps are async callables. They receive a `scope` (the request details) and two async functions (`receive` and `send`). Because it is an `async def`, you can `await` database calls in the middle of it.

The practical result: FastAPI

from fastapi import FastAPI
import asyncio

app = FastAPI()  # This creates an ASGI application under the hood

@app.get("/")
async def read_root():
    # The event loop can serve other requests while this sleeps
    await asyncio.sleep(2) 
    return {"message": "Hello World"}

You rarely write raw ASGI code. Frameworks like FastAPI abstract the `scope/receive/send` boilerplate. When you run this with Uvicorn, the async sleep proves the single thread is not blocked.

Key points

Common mistakes

Recall questions

Questions & answers

WSGI vs ASGI / why is FastAPI async?

WSGI is synchronous (one request blocks one worker). ASGI is asynchronous (one worker can handle many requests by awaiting I/O). FastAPI is built on ASGI to support `async/await`, allowing it to handle massive concurrent I/O loads (like database queries) without needing a huge threadpool.

Approach: Contrast the execution models (blocking vs event loop). Emphasize that ASGI allows yielding control during I/O, which is the secret to FastAPI's high concurrency.

You deployed a FastAPI app using standard Gunicorn, but it crashes on startup with an error about the callable not returning an iterable. What went wrong?

Gunicorn is a WSGI server by default and expects a synchronous callable (like a Flask app). FastAPI is an ASGI app. You must tell Gunicorn to use an ASGI worker class (like `uvicorn.workers.UvicornWorker`) to bridge the gap.

Approach: Recognize the mismatch between a WSGI server and an ASGI application. The fix is configuring the worker class.

Continue learning

Previous: Coroutines, async / await

Previous: The event loop

Next: async vs def Endpoints

Next: Uvicorn, Gunicorn, and Workers

Return to Python Backend Roadmap