The FastAPI Request Lifecycle Model
FastAPI is often described feature by feature: async support, automatic docs, dependency injection, Pydantic validation.
Those features are real, but they are not separate add-ons bolted onto a plain web framework.
They are one lifecycle: a request enters as an ASGI event, is matched to a path operation whose dependency graph was already resolved at startup, has its inputs validated before your function body runs, and has its outputs re-validated and filtered before they leave.
Understanding FastAPI well means understanding that lifecycle as a single pipeline, not as a checklist of independent capabilities.
This page builds that mental model; FastAPI Basics and Dependency Injection cover the hands-on syntax that this model explains from underneath.
Summary
- A FastAPI request moves through one lifecycle - ASGI dispatch, dependency resolution, Pydantic validation, handler execution, response serialization - and each stage's behavior only makes sense in light of the others.
- Insight: Most confusing FastAPI behavior (why a sync function didn't block, why validation errors never reach your code, why a dependency ran twice) traces back to a stage of this lifecycle being misunderstood, not a bug.
- Key Concepts: ASGI, the dependency graph, Pydantic validation, the sync/async threadpool split, response_model filtering.
- When to Use This Model: Reasoning about where validation happens, deciding whether a handler should be
async def, debugging why a dependency's teardown ran at an unexpected time, or explaining why FastAPI needs Starlette and Pydantic underneath it. - Limitations/Trade-offs: The lifecycle's flexibility comes from runtime introspection and per-request dependency resolution, both of which add measurable overhead compared to a framework with no validation or DI layer at all.
- Related Topics: Starlette routing and middleware, WSGI's synchronous request model, Pydantic's validation engine, Uvicorn and ASGI servers generally.
Foundations
FastAPI is not a server, and it is not even really a full framework in the sense Django is.
It is a typed request/response layer built on top of Starlette, an ASGI toolkit that already provides routing, middleware, and the test client, plus Pydantic, which provides the data validation and serialization engine.
ASGI (Asynchronous Server Gateway Interface) is the piece that makes this possible: unlike WSGI's one-request-per-call synchronous contract, an ASGI application is a single async callable that receives a scope, a receive awaitable, and a send awaitable, and it can hold a connection open, await I/O without blocking other requests, and handle WebSockets using the same interface.
A useful way to picture the relationship: Starlette is the engine that runs the ASGI protocol and finds the right function to call; FastAPI is what happens when you also enforce that the function's inputs and outputs are typed and validated.
That framing matters because a large share of what people call "FastAPI features" - path/query typing, request bodies, response filtering - are really Pydantic features that FastAPI wires into Starlette's routing at the right points in the lifecycle.
Mechanics & Interactions
The lifecycle actually has two phases: one that runs once at startup, and one that runs on every request.
At startup, FastAPI inspects each path operation function's signature - its parameters, their type hints, and any Depends(...) defaults - and builds a dependency graph for that route, including sub-dependencies of dependencies, all resolved ahead of time so the per-request work is just walking a known tree.
Request in
-> ASGI server (Uvicorn) hands the scope to the app
-> Starlette router matches path + method
-> FastAPI walks the pre-built dependency graph (top-down, sub-deps first)
-> Pydantic validates path/query/body params against the route's models
-> path operation function runs (async on the loop, sync in a threadpool)
-> return value validated/filtered against response_model
-> response sent; yield-style dependencies run teardown afterEach request then walks that graph: dependencies resolve top-down, a given dependency callable is only invoked once per request even if multiple other dependencies depend on it, and any dependency defined with yield has its post-yield code deferred until after the response has been generated.
The sync/async split is a core mechanic, not a style choice: an async def path operation or dependency runs directly on the event loop, while a plain def one is automatically dispatched to an external threadpool so it cannot block other concurrent requests - this is why "just don't use async" does not, by itself, make an endpoint scale worse, though it does cap concurrency to the threadpool size.
Validation happens before your function body executes, not inside it: FastAPI parses the raw ASGI body, matches it against the parameter's Pydantic model or scalar type, and raises a 422 with field-level errors automatically if it fails, so a handler only ever sees already-valid, already-typed data.
from fastapi import Depends, FastAPI
app = FastAPI()
def get_settings():
return {"env": "prod"}
def get_client(settings: dict = Depends(get_settings)):
return {"base_url": settings["env"]}
@app.get("/status")
def status(client: dict = Depends(get_client)):
return client # get_settings and get_client each ran once, not twiceAdvanced Considerations & Applications
Because the dependency graph and Pydantic models are resolved through Python's introspection machinery, FastAPI pays a real per-request cost that a framework with no validation layer does not: this is the trade-off for getting automatic OpenAPI schemas, editor autocompletion, and boundary safety for free.
The threadpool that backs sync path operations has a finite size (Starlette defaults it to a bounded worker pool), so an application that is mostly sync functions doing blocking I/O will eventually queue behind that pool under load, which is a scaling ceiling that is easy to miss until traffic increases - see Async Routes & Databases for the async-driver alternative.
response_model is not just documentation metadata: FastAPI actually re-validates and filters the object your function returns against it, which is why returning an ORM instance directly is safe from a data-leak perspective as long as the response model excludes sensitive fields, but it also means a mismatched return value fails at the response stage, not the request stage.
Background tasks (BackgroundTasks) run after the response has been sent to the client but still inside the same worker/request lifecycle, which makes them suitable for logging or fire-and-forget notifications but not for anything the client needs to know succeeded - see Background Tasks & Workers for where that boundary sits.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| FastAPI's ASGI + DI + Pydantic lifecycle | Automatic validation, docs, and typed DI with native async | Per-request introspection and validation overhead; sync deps capped by threadpool size | APIs where input safety, schema generation, and async I/O all matter |
| Plain Starlette (no FastAPI layer) | Minimal overhead, full ASGI control | No automatic validation, DI, or OpenAPI - all hand-rolled | Latency-critical services that don't need typed request contracts |
| WSGI framework (e.g. Flask) | Simpler mental model, mature sync ecosystem | No native async concurrency; blocking I/O ties up a worker | Sync-first apps without high concurrent I/O demands |
Common Misconceptions
- "Marking a route
async defautomatically makes it faster." It only helps when the handler is actually awaiting I/O; a CPU-boundasync defstill blocks the single event loop for every other concurrent request, which a syncdefrunning in the threadpool would not do. - "Depends is just a way to call a helper function." A dependency is resolved once per request, cached for that request if reused elsewhere in the same graph, and can be overridden wholesale in tests via
app.dependency_overrides- none of which a plain function call gives you. - "Validation happens inside my endpoint function." It happens before your function is ever invoked; by the time your code runs, the data has already passed Pydantic's checks, which is why raising your own 422 by hand is rarely necessary.
- "response_model is only for the OpenAPI docs." It actively filters and re-validates the return value at runtime, which is the mechanism that keeps a field like a password hash out of a JSON response even if the returned object still has that attribute.
- "FastAPI is a from-scratch web server." It has no server of its own; Uvicorn (or another ASGI server) is what actually accepts connections, and FastAPI is the typed application that server calls into.
FAQs
What does "ASGI" actually mean for how FastAPI handles a request?
It means the application is a single async callable receiving scope, receive, and send, which lets FastAPI hold a connection open and await I/O (database calls, HTTP calls, WebSocket messages) without blocking other concurrent requests on the same event loop.
Is FastAPI its own web server?
No. Uvicorn or another ASGI server accepts the actual TCP connections and calls into the FastAPI application object; FastAPI's job starts once a request has already reached it as an ASGI event.
When is a route's dependency graph built - per request, or once?
Once, at startup, when FastAPI inspects each path operation's signature and its Depends(...) defaults. Per request, it only walks that already-known graph rather than re-discovering it.
Does a dependency used by two other dependencies run twice in one request?
No. FastAPI resolves and caches each dependency callable once per request by default, so shared sub-dependencies (like a database session) are computed a single time even if several other dependencies need them.
Should every path operation be `async def`?
Only if the function actually awaits I/O. A CPU-bound async def blocks the whole event loop for the duration of that work; a sync def doing the same CPU work runs in a threadpool instead, which does not block other requests' async work.
What happens if request data fails Pydantic validation?
FastAPI returns a 422 response with field-level error details automatically, before your path operation function's body ever executes - your code never sees invalid input.
What does `response_model` actually do at runtime?
It validates and filters the value your function returns against that model, stripping any fields not declared on it, which is why it is used to prevent internal-only fields from leaking into a JSON response.
When do `yield`-based dependency teardown blocks actually run?
After the response has already been generated (and, depending on version and setup, potentially after it has been sent), which makes them suitable for closing a database session or connection but unsuitable for anything that must affect the response body itself.
Why would a sync path operation ever be a scaling risk?
Because it runs in a bounded threadpool; once concurrent sync requests exceed the pool's size, additional ones queue behind it, creating latency that async I/O-bound code sharing the event loop would not incur.
Can I override a dependency for testing?
Yes - app.dependency_overrides[some_dependency] = fake_dependency replaces it for the whole app or test session without touching route code, which only works because dependencies are resolved through this graph rather than called directly.
Do background tasks delay the response to the client?
No, they run after the response has been sent, which is exactly why they are appropriate for logging or notifications but not for anything the caller needs confirmed before the request completes.
Why does FastAPI need both Starlette and Pydantic?
Starlette supplies the ASGI routing, middleware, and dispatch mechanics; Pydantic supplies the data validation and serialization engine. FastAPI's contribution is wiring typed request/response models into Starlette's dispatch points and generating OpenAPI from them.
Related
- FastAPI Basics - hands-on routes, params, and automatic docs
- Dependency Injection -
Depends, sub-dependencies, and test overrides in practice - Request & Response Models - Pydantic models at the request/response boundary
- Async Routes & Databases - the async I/O side of the sync/async split
- Middleware & Error Handling - where middleware sits relative to this lifecycle
- Background Tasks & Workers - what runs after the response is sent
Stack versions: This page was written for FastAPI 0.115+ on Python 3.14 (maintenance: 3.13), using Pydantic 2 for validation.