All posts
Article·June 5, 2026·10 min read

How FastAPI actually works: ASGI, async and dependency injection

A look under the hood of FastAPI — the ASGI contract that replaced WSGI, how the event loop turns async def into concurrency, and how Starlette, Pydantic, and dependency injection fit together over the life of a request.

  • FastAPI
  • Python
  • Backend

FastAPI feels almost too convenient. You annotate a function, add some type hints, and you get validation, serialization, docs, and async concurrency for free. It is worth understanding what is doing that work, because “free” features have failure modes, and the ones in FastAPI mostly trace back to three ideas: the ASGI protocol, the async event loop, and dependency injection. Get those and the framework stops being magic.

WSGI could not do async, so ASGI was invented

For most of Python’s web history, the contract between a server and an application was WSGI. It is a simple, synchronous function: the server hands your app an environment dict and astart_response callable, your app returns a response body, and the server ships it. Simple, but fundamentally blocking. One request occupies one worker until it finishes. If that request spends 300ms waiting on a database, the worker sits idle the entire time.

ASGI (Asynchronous Server Gateway Interface) is the async successor. Instead of a return-a-value function, an ASGI app is a coroutine that receives three arguments — scope (metadata about the connection), plus receive and send awaitables for streaming events in and out. That shape is what lets a single process handle many in-flight requests at once, and it is why ASGI can also carry WebSockets and long-lived connections that WSGI never could.

python — the raw ASGI shape FastAPI implements for you
async def app(scope, receive, send):
    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",
    })

You never write that by hand. FastAPI is built on Starlette, which implements the ASGI plumbing — routing, middleware, requests, responses — and FastAPI layers typing, validation, and docs on top. A server like Uvicorn speaks HTTP on one side and calls your ASGI app on the other.

ClientHTTP requestUvicornASGI serverMiddlewareCORS, auth…Routermatch pathHandleryour coderesolve dependencies +validate body (Pydantic)response travels back out the same path
Fig 1 — The request lifecycle. Uvicorn speaks HTTP; everything to its right is your ASGI application, resolved dependencies first.

The event loop, and why one process can juggle thousands

The word that trips people up is “async.” It does not mean parallel and it does not mean threads. A FastAPI process runs a single event loop (via asyncio) on one thread. When your async def handler hits an await on something that waits — a database round trip, an HTTP call to another service — the coroutine yields control back to the loop. The loop is then free to run another request that is ready, and it comes back to yours when the awaited operation completes.

This is cooperative concurrency: tasks voluntarily hand control back at await points. It wins big for I/O-bound work, where most of the time is spent waiting on the network or disk rather than the CPU. One process can hold thousands of connections open because most of them are parked at an await, costing nothing while they wait.

python — a route and a typed model
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float
    in_stock: bool = True

@app.post("/items")
async def create_item(item: Item):
    # 'item' is already parsed, validated and typed.
    # A malformed body never reaches this line — FastAPI
    # returns a 422 with a precise error before we run.
    return {"name": item.name, "total": item.price}

Notice what those type hints bought. Because item is annotated as an Item, FastAPI knows to read the request body, hand it to Pydantic for validation and coercion, and reject anything that does not fit — all before your function body executes. The same type information generates the OpenAPI schema and the interactive docs. The hints are not decoration; they are the configuration.

Dependency injection: the part that scales your codebase

Dependency injection is FastAPI’s most underrated feature. Instead of a handler reaching out to fetch its database session, current user, or config, it declares what it needs as a parameter with Depends(...), and FastAPI supplies it.

python — a dependency, and a route that consumes it
from fastapi import Depends, Header, HTTPException

async def get_current_user(authorization: str = Header(None)):
    if not authorization:
        raise HTTPException(status_code=401, detail="Missing token")
    user = await lookup_user(authorization)
    return user

@app.get("/me")
async def read_me(user = Depends(get_current_user)):
    return {"id": user.id, "email": user.email}

Before read_me runs, FastAPI sees the Depends, calls get_current_user, and passes the result in. If the dependency raises, the handler never executes. Dependencies can depend on other dependencies, so the framework resolves the whole tree in order. This is why cross-cutting concerns — auth, DB sessions, rate limits, pagination — end up as small, testable, reusable functions rather than boilerplate copied into every route.

Dependencies that yield are especially useful: the code before the yield runs on the way in, and the code after runs on the way out, which is exactly the shape you want for opening and reliably closing a resource.

python — setup/teardown with a yielding dependency
async def get_db():
    session = SessionLocal()
    try:
        yield session          # handler runs with the session
    finally:
        await session.close()  # always runs, even on error

Putting it together

A request now has a clear story. Uvicorn accepts the HTTP connection and invokes the ASGI app. Starlette runs middleware and matches the route. FastAPI resolves the dependency tree and validates the body with Pydantic. Your handler runs on the event loop, yielding at each await so other requests keep moving. The return value is serialized, the yielding dependencies tear down, and the response travels back out.

None of it is magic. It is a well-chosen protocol (ASGI), a concurrency model that suits web workloads (the event loop), and a resolution system that keeps code composable (dependency injection). Once you can see those three pieces, both the ergonomics and the sharp edges — the blocking call that freezes the loop, the dependency that must clean up after itself — stop being surprises.