FastAPI vs. Django vs. Flask: Request Lifecycle, Dependency Injection, and Async Model
Concept
The three dominant Python web frameworks differ less in "can it serve HTTP" than in three structural decisions: what gateway interface they run on, how a handler declares what it needs, and how much the framework decides for you versus leaves open.
- Gateway interface. Flask and (classically) Django run on WSGI — a synchronous interface where one worker thread/process blocks for the full duration of a request, including any I/O wait. FastAPI is ASGI-native from the ground up; Django added ASGI support and async views/middleware on top of a codebase whose ORM and much of its ecosystem still assume sync. WSGI's blocking model means concurrency comes from spinning up more workers (processes/threads), each with real memory overhead; ASGI lets one worker interleave many in-flight requests during I/O waits (a DB query, an external API call), because the worker isn't parked waiting — it's servicing another request's coroutine in the meantime.
- Dependency injection. FastAPI's
Depends()is a first-class part of the framework: a handler declares the objects it needs (a DB session, the current user, a validated query-param model) as function parameters, and FastAPI resolves the dependency graph per-request, including dependencies that themselves depend on other dependencies, and supports async dependencies natively. Flask has no built-in DI — request-scoped state goes through thegobject or is done by hand with decorators/context managers. Django has no DI system either; the closest analogue is middleware (global, applies to every request) and class-based view mixins (opt-in per view, but not composable the wayDepends()chains are). - Structural philosophy. Django is "batteries-included": ORM, admin panel, auth, forms, migrations are all decided for you, which is exactly the value proposition — a new Django app has a working admin UI and user model on day one. Flask is a microframework: routing and request/response objects only, everything else (ORM, validation, auth) is a library you choose and wire up yourself. FastAPI sits in between — opinionated about the API boundary (Pydantic models for request/response validation,
Depends()for injection, automatic OpenAPI docs) but silent on everything behind that boundary (ORM, background job system, project layout are your choice).
Tradeoffs
| Framework | Concurrency model | Dependency injection | Best for | Cost |
|---|---|---|---|---|
| FastAPI | ASGI-native, async-first (sync handlers also supported) | Built-in Depends(), composable, async-aware |
High-throughput APIs with I/O-bound work (DB calls, calls to other services), typed request/response contracts | Younger ecosystem than Django's for things Django ships built-in (admin, ORM, auth) — you assemble those yourself |
| Django | WSGI by default; ASGI + async views available, but ORM and much of the ecosystem still assumes sync | None — middleware (global) and CBV mixins (per-view) instead | Content-heavy or CRUD-heavy apps that want an admin panel, ORM, auth, and forms out of the box, and don't need to push async all the way through | Retrofitted async is uneven — an async view that calls the sync ORM re-blocks the event loop unless you explicitly wrap it (sync_to_async), so "async Django" doesn't remove the sync ORM as the real bottleneck |
| Flask | WSGI (sync); async view functions exist but run inside the sync WSGI worker model, not a true ASGI event loop | None — g/context managers/decorators by hand |
Small services, prototypes, or apps that want to hand-pick every piece of the stack | No enforced structure means dependency wiring and validation are 100% the team's own discipline to get and keep consistent |
The real fork isn't "which is fastest" in isolation — it's whether the workload is I/O-bound at scale (favors ASGI: FastAPI, or Django-with-async-views-done-carefully) and whether the team wants the framework to hand it a DI system and validation layer (FastAPI) or wants everything else decided for them (Django) or wants nothing decided for them (Flask).
When to use / when not to
- Reach for FastAPI when the service is API-first (JSON in, JSON out), does non-trivial I/O-bound work per request (calls another service, hits a DB, calls an LLM), and the team wants request/response shapes enforced by the framework rather than by convention.
- Reach for Django when the app needs an admin UI, a mature auth/permissions system, and an ORM with migrations on day one, and the workload is closer to a traditional CRUD web app than a high-fan-out API — the "batteries" are the actual point.
- Reach for Flask when the service is small enough that a microframework's freedom is worth more than a batteries-included framework's defaults, or when the team already has strong opinions about which ORM/validation/auth libraries to use and doesn't want a framework fighting those choices.
- Don't pick FastAPI purely for "it's async" if the actual work is CPU-bound (heavy computation per request) — async only helps I/O-bound concurrency; a CPU-bound handler blocks the event loop the same way a sync handler blocks a WSGI worker, and the fix in both cases is offloading to a worker pool, not switching frameworks.
- Don't bolt async views onto Django expecting a free throughput win if the ORM calls inside them are still synchronous — the sync ORM call re-blocks the loop unless explicitly wrapped, at which point you've reintroduced the blocking you were trying to avoid.
Common pitfall
Treating FastAPI's Depends() graph as free architecture instead of an actual dependency graph that needs the same discipline as any other DI system. A dependency that itself depends on three other dependencies, each doing their own DB round-trip, composes cleanly in code but can produce N sequential round-trips per request that aren't visible from reading any single function — the graph has to be read as a whole, not handler-by-handler, to see the real request-time cost. The fix is the same one DI systems have always needed: keep the dependency graph shallow, batch or cache dependencies that would otherwise run redundantly within one request, and treat "three layers of Depends() deep" as a refactor signal rather than a sign the framework is working as intended.
Engineering Lens
The FastAPI/Django/Flask choice is a proxy for a bigger question every team answers implicitly: how much should the framework decide, and how much should the team decide? Django answers "the framework decides most of it," which is a genuine productivity win until the app's shape diverges from what Django assumed (an API-only service with no admin UI, no server-rendered templates, and a non-relational primary store fights Django's grain the whole way). Flask answers "the team decides all of it," which avoids that mismatch entirely but means every project reinvents its own validation/DI/auth conventions, with real variance in quality across teams. FastAPI's bet is a narrower one — decide the API boundary (validation, injection, docs) and stay silent on everything behind it — which is why it fits API-first services particularly well without becoming a full opinionated stack like Django's. None of the three is "correct"; the mismatch between a project's actual shape and a framework's assumptions is where real rewrites come from, not from picking the objectively worst framework.
Sources
- Python API Development: FastAPI vs Flask vs Django Framework — Cloudways
- FastAPI vs Flask: Key Differences, Performance, and Use Cases — Codecademy
- FastAPI vs Django vs Flask in 2026: Choosing the Right Python Web Framework