Skip to content
Home About Services Work Notes Contact Start a project
Backend architecture

Django or FastAPI: Choosing by the Shape of the Problem

Both are good. They are good at different things, and the benchmark everyone quotes measures the part of your latency budget you were never going to spend.

DjangoFastAPIPythonArchitecture

The comparison usually opens with a benchmark chart, and the chart is usually irrelevant. It measures framework overhead on an endpoint that returns a hard-coded dictionary — a few hundred microseconds of a request that will spend 40ms in the database and 200ms crossing the Indian mobile network.

If framework overhead is your bottleneck, congratulations: you have solved every other problem. For everyone else, the decision comes down to the shape of what you are building.

The question that actually decides it

Is there a back office?

Not "is there an admin panel on the roadmap" — is there a group of people whose job is to look at this data, correct it, approve it, and export it? A hospital billing desk, a school fee office, a moderation team, an operations desk.

If yes, Django's admin is worth more than every other consideration on this page combined. You get a permissioned, searchable, filterable CRUD interface over your entire data model, for free, on day one. Rebuilding a fraction of that on FastAPI is weeks of work that delivers no differentiated value.

If no — if the system's only consumers are a mobile app, a React front end and some other services — then Django's biggest advantage does not apply, and the calculation changes.

What each one hands you

DjangoFastAPI
Admin interfaceBuilt in, production-gradeNone
ORM + migrationsBuilt in, autogeneratedSQLAlchemy + Alembic, wired by you
Auth, sessions, permissionsBuilt inLibraries, assembled by you
Request validationForms / DRF serializersPydantic, deeply integrated
API docsAdd-onOpenAPI generated from types
AsyncSupported, ORM partiallyNative, end to end
Server-rendered HTMLFirst-classPossible, not the point

Read that table as a statement about where the work goes, not about quality. Django decides a lot for you and you live with those decisions. FastAPI decides very little and you make those decisions — which is freedom when you have an opinion and overhead when you do not.

Where async genuinely wins

Async is not faster at CPU work and it is not faster at a single database query. It wins when a request spends most of its life waiting on something else, and you have many such requests at once.

Concretely: a request that calls three external APIs, or that streams tokens from a language model for thirty seconds, or that holds a WebSocket open. A sync worker is fully occupied for that whole duration. An async worker handles hundreds concurrently because it is idle during the wait.

# Three independent calls. Sync: sum of the latencies.
# Async: the slowest one.
@app.get("/dashboard")
async def dashboard(user_id: int):
    rates, weather, inventory = await asyncio.gather(
        fetch_rates(),
        fetch_weather(),
        fetch_inventory(user_id),
    )
    return {"rates": rates, "weather": weather, "inventory": inventory}

This is the profile of AI and ML services almost by definition — they are mostly waiting on a model endpoint — which is why FastAPI has become the default there. It is also why an LLM feature bolted onto a Django app is worth thinking about carefully: a streaming endpoint tying up a sync worker for thirty seconds is an expensive way to hold a socket open.

Django's async is real but partial

Django supports async views, middleware and an async ORM interface. The caveat is that the ORM's async methods are adapters over synchronous database drivers — the call does not block the event loop, but you are not getting async all the way down to the socket. It is genuinely useful for views that mostly await HTTP calls. It is not a reason to choose Django for a workload that is fundamentally async.

Be careful with the failure mode: calling a sync ORM method inside an async view raises SynchronousOnlyOperation, and the fix is sync_to_async, at which point you have a thread pool and much of the benefit has evaporated. Async Django works best when a view is async for a clear reason, not as a default.

Use both, on purpose

The configuration I reach for most on larger systems is not a choice at all:

Django                          FastAPI
  admin + back office             inference / streaming endpoints
  business logic + ORM            heavy async I/O
  server-rendered pages           WebSockets
  Celery producers
        |                                |
        +-------- one PostgreSQL --------+
                  one Redis

Django owns the domain model and the people-facing surface. FastAPI owns the endpoints that are I/O-bound or streaming. They share a database, and the FastAPI service reads through its own thin data layer rather than importing Django models — that boundary is what stops it becoming two applications tangled into one.

The cost is real: two deployments, two dependency sets, two sets of logs. Do not do it for elegance. Do it when there is a specific workload that is genuinely async-shaped.

Choosing, concretely

Django for line-of-business software, anything with a back office, anything where staff correct data, and anything that renders HTML. Hospital systems, school ERPs, CRMs, marketplaces, most SaaS. The admin alone usually settles it.

FastAPI for pure APIs with no human-facing admin, ML and LLM services, streaming, WebSockets, and internal services where the OpenAPI schema is the contract with another team.

Neither, on vibes. If someone argues from a benchmark, ask what fraction of their p95 latency is framework overhead. The answer is almost always "less than the error bar".

Next step

Need this built properly?

Tell me what you are trying to ship. You get an approach, a timeline and a realistic estimate — usually within a working day.