Skip to content
Beyond the Stack

Inside FastAPI · Part 1/2

فارسی

What happens when an HTTP request reaches FastAPI?

A single GET request crosses a socket, an ASGI server, a router, a dependency graph, and a serializer. Here is what each layer does, and where it can fail.

Published 4 min readUpdated

You start the process with uvicorn app.main:app --port 8000, run curl localhost:8000/items/42, and get JSON back. The gap between those two commands hides a fixed sequence of handoffs. Most production surprises in a FastAPI service come from not knowing which component owns which part of that sequence.

The socket belongs to the server

The client opens a TCP connection, and Uvicorn accepts it. Uvicorn parses the request line, headers, and body according to HTTP/1.1 rules, then constructs an ASGI scope: a dictionary containing the method, path, query string, headers, client address, and protocol metadata. FastAPI never sees a socket, and it does not parse HTTP. That separation is why the same application object can run under Uvicorn, Hypercorn, or another ASGI server.

Pipeline from client through Uvicorn, the ASGI boundary, router, dependency graph, endpoint, serialization, and response
The request path through a FastAPI application. The dashed region is the ASGI application.

The ASGI boundary

ASGI defines a three-part call: the application receives a scope, an async receive callable for incoming events, and an async send callable for outgoing events. For an HTTP request, the server calls receive to deliver http.request messages containing the body, and the application calls send with http.response.start and one or more http.response.body messages.1 Middleware wraps this interface, so a middleware that reads the body must put it back for the next layer.

Routing is Starlette’s job

FastAPI’s @app.get decorator ultimately registers an APIRoute with Starlette’s router. The router walks its route list in order and matches the path. A static path should be declared before a parameterized path that would also match it, because the first match wins. Once a route matches, Starlette calls its handler; for an APIRoute, that handler is FastAPI’s request handler, which performs dependency resolution and input validation.

Dependencies run before your function

The endpoint signature is inspected when the route is registered. Each Depends(...) becomes a node in a dependency tree, including dependencies of dependencies. On every request, FastAPI solves that tree, validates path, query, header, and body inputs, and only then calls your endpoint. The next article in this series follows that tree in detail.

from fastapi import Depends, FastAPI

app = FastAPI()

def get_settings() -> dict[str, str]:
    return {"region": "eu-west-1"}

@app.get("/items/{item_id}")
async def read_item(item_id: int, settings: dict = Depends(get_settings)):
    return {"item_id": item_id, "region": settings["region"]}

Endpoint, serialization, response

An async def endpoint is awaited directly on the event loop. A plain def endpoint is run in a worker thread from a bounded thread pool, because it may block. The return value is then validated against response_model if one is declared, converted to JSON-compatible data, and rendered by the response class. Finally the application calls send, and Uvicorn writes the bytes to the socket.

Stage Owned by Typical failure
Accept and parse Uvicorn malformed request, oversized headers
Route match Starlette router 404, wrong route order
Dependencies FastAPI 401, 422, pool timeout
Endpoint your code raised exception, blocking call
Serialization Pydantic response validation error

What changes under load

The stages above are cheap individually and expensive together. A blocking call inside an async def endpoint stalls every request on that event loop, because the loop cannot schedule other work while the call runs. A plain def endpoint avoids that, but it competes for a limited thread pool. Dependency code that opens a database session without closing it turns a latency problem into an exhaustion problem under concurrency.

What to carry forward

The request path is a chain of narrow contracts: HTTP to ASGI, ASGI to router, router to dependency graph, graph to endpoint, endpoint to serializer. When something breaks, identify the stage before changing code. Most reports that “FastAPI is slow” are really event-loop blocking, thread-pool saturation, or a dependency that does more work than its name suggests.

Footnotes

  1. The ASGI HTTP specification defines the scope keys and the http.request and http.response.* message types that a server and an application exchange. ↩