If your Django app is slow, the reason is almost always the database, not Django and not Python. Specifically: N+1 queries, missing select_related and prefetch_related, missing indexes, missing caching, and an ORM used without watching the SQL it generates. Fix those, and the same endpoint moves from a 2-second p95 to under 200 milliseconds. We have done this dozens of times.

The honest summary: Django is not slow. Slow Django apps are slow because of the same seven decisions on every project. Below we walk through them in order of impact — with the concrete ORM patterns and the order to apply them.

Measure first

Before you fix anything, install Django Debug Toolbar in development or django-silk in any environment, and look at where the time actually goes per request. Record the query count, the slowest queries, the total query time, and the p50, p95, and p99 response times on your top three endpoints. These are the numbers you compare against after each fix.

The number that matters most for a backend is p95 latency. The median hides the slow tail; p99 is too noisy on small traffic. p95 is the right balance. A healthy Django endpoint hits p95 under 200 milliseconds; a slow one sits over a second and stays there.

Fix 1 — Eliminate N+1 queries with select_related and prefetch_related

The single biggest source of slowness in Django apps is the N+1 query problem. The pattern: you fetch a queryset, loop over it, access a related object, and the ORM fires a fresh query for every row. Fetching 50 articles and reading article.author on each triggers 1 query for the articles plus 50 for the authors — 51 queries to render one list.

The fix is two ORM methods. Use select_related('author') for forward ForeignKey and OneToOne relations — it does a single SQL JOIN. Use prefetch_related('comments') for reverse ForeignKey and ManyToMany relations — it runs one extra query and joins the results in Python. Both chain and nest: Article.objects.select_related('author').prefetch_related('comments') renders the whole list in three queries regardless of row count.

Catch N+1 automatically rather than by eye. The nplusone package raises on lazy loads in development, and asserting an exact query count inside your tests stops the regression coming back the next time someone adds a template tag.

Fix 2 — Add database indexes

Every column you filter or order by should have an index. Django indexes ForeignKey columns for you; the ones teams miss are status fields, slugs, boolean flags, and the created_at column every list view orders by. On a table past a few thousand rows, a missing index turns a 2-millisecond lookup into a 200-millisecond scan.

The fix is a database audit. Run EXPLAIN ANALYZE on each slow query — django-silk shows you which ones. Look for sequential scans on large tables. Add indexes through Meta.indexes with models.Index, and use multi-column indexes for queries that filter on two columns at once: a query that filters on tenant_id and status together wants a compound index on both, not two separate ones.

Fix 3 — Cache the expensive parts

The fastest query is the one you never run. Django’s cache framework, backed by Redis, makes caching cheap to add: cache.get_or_set('dashboard:' + str(user.id), expensive_query, 600) stores the result for ten minutes. Django also gives you per-view caching and template fragment caching for the parts of a page that rarely change.

What to cache: dashboard aggregations that recompute the same totals on every page load, expensive serialisations, anything that calls an external API. What not to cache: per-user data that changes constantly, anything that must be real-time. Use cache keys you can target precisely, so that when the underlying data changes you invalidate exactly what is stale and nothing more.

Fix 4 — Move slow work to a task queue

Anything that takes more than a few hundred milliseconds and does not need to finish before the response should not happen in the request. Sending email, generating PDFs, calling external APIs, processing uploads, recomputing analytics — all of it belongs in a background task.

Celery with a Redis or RabbitMQ broker is the standard answer; for smaller setups, RQ or Django’s own background-task options are simpler. The shape is the same: send_notification.delay(user.id) returns immediately, a worker handles the job out of band, and the user sees a fast response. Moving synchronous work off the request routinely cuts p95 latency by 40 to 70 percent.

Fix 5 — Stop the ORM over-fetching

The Django ORM is convenient, and sometimes too convenient. A handful of habits quietly cost performance. Calling .all() and then using three fields hydrates every column on every row — use .only('id', 'name', 'email') or .values() instead. Building model objects when you only need raw numbers wastes work — use .values() and .values_list() for reports.

Two more: len(queryset) loads every row into memory to count it, while .count() runs a single SELECT COUNT(*); and checking truthiness on a queryset loads it, while .exists() asks the database a yes-or-no question. Each fix is a small habit, and together they compound across every view in the app.

Fix 6 — Go async where it pays

Django runs under ASGI and supports async views. Async helps one specific thing well: a view that waits on several external services can await them concurrently instead of in series, so an endpoint calling three APIs finishes in the time of the slowest one rather than the sum of all three.

Be clear about what async does not do. It does not make database queries faster, and Django’s async ORM story is still partial. If an endpoint is slow because of N+1 queries, async will not save it — fix the queries. Reach for async views when the bottleneck is genuinely I/O fan-out, not as a blanket performance setting.

Fix 7 — Profile the production database

The last fix is about infrastructure, not application code. Turn on pg_stat_statements on PostgreSQL, or the slow query log on MySQL, and let it run for a week. Then look at the queries that consume the most total time across all calls — often the culprit is one or two queries that each take 40 milliseconds but run tens of thousands of times.

The remedy varies: add an index, rewrite the query, denormalise a column, add a materialised view, or cache the result. The diagnostic is always the same — find the top five queries by total time and fix them one at a time.

What good looks like after the seven fixes

A well-tuned Django application in 2026 hits these numbers: p95 latency under 200 milliseconds on real production traffic; query count under 10 per read request and under 20 on writes; cache hit ratio above 80 percent on cacheable endpoints; task queue backlog under 100 jobs at any moment; and memory per request that stays flat as traffic grows.

These are the targets we hold every Django build to. Performance is not a phase-two item — it is part of the build, and the seven fixes above are the order we apply it in.

Common questions

How much will the seven fixes improve my Django app?

On a typical slow Django app with a p95 of 1 to 2 seconds, the seven fixes together usually move p95 under 200 milliseconds and unlock several times the throughput. The single biggest gain is almost always eliminating N+1 queries with select_related and prefetch_related. Apps with synchronous external calls gain most from moving work to a task queue; apps with hot dashboards gain most from caching. Exact numbers depend on your starting state, but a halving of p95 within the first two weeks is normal.

Is Django’s ORM the problem, or is it Python?

Neither, in almost every case. The Django ORM generates efficient SQL when you use it correctly — the slowness comes from N+1 queries and over-fetching, which are usage patterns, not framework faults. Python itself is rarely the bottleneck in a web request, because the request spends its time waiting on the database, not running Python. The fix is to watch the SQL the ORM generates and shape your querysets, not to abandon the ORM or the language.

Should I rewrite my slow Django app in FastAPI?

Almost never, if speed is the only reason. A slow Django app rewritten in FastAPI with the same database problems will still be slow, because the bottleneck is the queries, not the framework. Fix the seven things first; most teams find the app is fast enough afterwards. FastAPI is the right move when you have a genuine async, high-concurrency workload — not as a cure for queries you have not optimised yet.

Does going async make Django faster?

Only for a specific workload: views that wait on several external services at once, which async lets you await concurrently. Async does not speed up database queries, and Django’s async ORM is still partial. If your endpoint is slow because of N+1 queries, async will not help — fix the queries. If it is slow because it calls three APIs one after another, async views are exactly the right tool.

How long does a Django performance engagement take?

The audit alone is typically 3 to 5 working days. Implementation depends on which of the seven fixes the audit prioritises and the app’s starting state — a typical engagement is 2 to 4 weeks of part-time work, usually delivered while the app stays live and changes are tested on staging first. Most clients see p95 latency halve within the first two weeks of focused work.

Want a free Django performance audit?

Send us a repository URL or staging access. We will send back a short audit showing your current p95 latency, the worst query offenders, and the highest-impact fixes for your app specifically.


Talk to our Python team