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.
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.
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
-
The ASGI HTTP specification defines the
scopekeys and thehttp.requestandhttp.response.*message types that a server and an application exchange. ↩