Why your Django app gets slow as it grows (and it's usually not Django)
· 7 min read
Every few months someone tells me Django doesn't scale. Almost every time, the actual problem turns out to be one of four things — and none of them are the framework's fault. Here they are in the order I check them.
1. N+1 queries
This is the single most common cause of a Django endpoint that was fine last quarter and is slow now. You loop over a queryset and touch a related object inside the loop, and the ORM obligingly issues one extra query per row. Twenty rows in development feels instant. Two thousand rows in production does not.
The fix is select_related for forward single-valued relationships (it does a SQL join) and prefetch_related for reverse or many-to-many ones (it does a second query and stitches the results in Python). The important habit isn't memorising which to use — it's noticing when a template or serializer reaches across a relationship at all.
The reason this bug is so persistent is that it's invisible in code review. Nothing about the line looks expensive. You have to be looking at query counts, not at the code.
2. Missing indexes
A query that filters or orders on an unindexed column forces PostgreSQL to scan the whole table. Like the N+1 problem, this scales with your data, so it stays hidden until the table is big enough to hurt — which is precisely when you least want to be diagnosing it.
Run EXPLAIN ANALYZE on your slowest queries and look for sequential scans on large tables. Add indexes for the columns you actually filter, order and join on. Be deliberate rather than exhaustive: every index makes writes slower and takes space, so indexing everything is its own problem.
3. Synchronous work that should be on a queue
Sending an email, calling a payment gateway, generating a PDF, syncing to a third-party service — if any of that happens inside the request/response cycle, your user is waiting on it. Worse, your response time is now hostage to somebody else's API being up.
Move it to a background worker. The request returns as soon as the work is accepted, not once it's finished. This tends to be the single biggest perceived-latency win available, because you're not optimising the work — you're removing it from the path the user is blocked on.
It also forces a healthier design: jobs have to become retryable and idempotent, which is exactly what you want for anything touching money or external systems.
4. Hot reads with no caching
Some queries run on nearly every request — a catalogue, a config lookup, a permissions check. They may each be individually fast and still dominate your database load simply by volume.
Cache them, but decide your invalidation story first. A cache without a clear answer to "what makes this stale, and what clears it?" trades a performance bug for a correctness bug, and correctness bugs are much harder to notice. If you can't explain when an entry gets cleared, you're not ready to add the cache yet.
Measure before you change anything
The mistake I see most often isn't picking the wrong fix — it's guessing which of these four is the problem. Optimising the wrong layer is worse than doing nothing, because you spend the effort and conclude the framework is at fault.
Before touching code:
- Count the queries a slow endpoint issues. A surprising number is the tell for problem #1.
EXPLAIN ANALYZEthe slowest ones. Sequential scans on big tables point at #2.- Ask what the request is waiting on. External calls point at #3.
- Look at query volume, not just query duration. High-frequency cheap reads point at #4.
Why this matters more with a time promise
At Fasto we promise delivery in ten minutes, which removes most of the slack you'd normally have. When the product itself is measured in minutes, backend latency stops being an engineering metric and becomes a customer-facing one.
That constraint is clarifying. It makes it obvious that reliability and performance work isn't polish you get to after features — it is the feature.
More on how I approach this in Django development and on the infrastructure side.