A slow Django application rarely needs a bigger server. Far more often it needs fewer, smarter database queries, a couple of indexes and some caching. This guide walks through a practical Django performance optimization process: measure first, fix the biggest problems, and only then scale hardware.
It applies to classic Django sites and to Django REST Framework (DRF) APIs serving mobile apps and SPAs. It reflects the kinds of fixes I make when clients ask me to speed up or stabilize an existing backend.
Step 0: Measure before you optimize
Guessing wastes time. Start by finding where time is actually spent.
- Django Debug Toolbar (development): shows SQL queries, duplicates and timings per request.
django-silkor APM tools (staging/production): profile requests, queries and Python time.- Database logs: enable slow query logging in PostgreSQL (
log_min_duration_statement). - Basic metrics: p50/p95 response times, error rate, and database CPU.
Pick the slowest and most frequently used endpoints. Optimize those first.
Problem 1: N+1 queries
The classic Django slowdown. You fetch a list of objects, then touch a related object inside a loop, triggering one extra query per row.
# Slow: 1 query for orders + 1 query per order for the customer
for order in Order.objects.all():
print(order.customer.name)Fix with select_related for foreign keys and one-to-one relations:
orders = Order.objects.select_related("customer")And with prefetch_related for many-to-many and reverse relations:
orders = Order.objects.prefetch_related("items")In DRF, apply these in the viewset's get_queryset(), because serializers that access related fields are the most common hidden source of N+1 queries. See also REST API best practices with Django.
Problem 2: Fetching more data than you need
- Use
.only()or.defer()to avoid loading large fields you do not use. - Use
.values()or.values_list()when you only need a few columns. - Use
.exists()instead oflen(queryset)orif queryset:for existence checks. - Use
.count()instead oflen(queryset). - Paginate everything. Never return an unbounded list from an API.
Problem 3: Missing database indexes
Queries that filter, order or join on unindexed columns scan entire tables. Add indexes where you filter often:
class Order(models.Model):
status = models.CharField(max_length=20, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=["customer", "-created_at"]),
]Use EXPLAIN ANALYZE in PostgreSQL to confirm that the query actually uses the index. Indexes speed up reads but slow writes slightly, so add them for real query patterns, not everywhere.
Problem 4: Doing slow work inside the request
Anything that takes more than a few hundred milliseconds and is not needed for the response should move to a background worker.
- Sending emails and notifications
- Generating PDFs or reports
- Calling slow third-party APIs
- Data imports and syncing
Use Celery with Redis and return a response immediately, then update the user when the job finishes. For a real-world example of background syncing with locks and circuit breakers, read building a sync engine for scripture.
Problem 5: Not caching what rarely changes
Caching removes repeated work entirely.
- Per-view or per-fragment caching for pages and expensive blocks.
- Low-level cache (
cache.get/set) for computed values and API responses. - Redis as the cache backend in production.
- Cache invalidation: set sensible timeouts, or invalidate on write for critical data.
Start with the endpoints that are read very often and change rarely, such as configuration, catalogs and public listings.
Problem 6: Inefficient bulk operations
Looping and saving one row at a time is slow.
Product.objects.bulk_create(products, batch_size=1000)
Product.objects.bulk_update(products, ["price"], batch_size=1000)Use update() for set-based changes, and wrap related changes in transaction.atomic() so they commit once.
Problem 7: Settings and infrastructure
- Turn off
DEBUGin production, since it keeps every query in memory. - Use persistent database connections (
CONN_MAX_AGE) or a connection pooler such as PgBouncer. - Serve static files through a CDN or a dedicated web server, not Django.
- Run enough Gunicorn or Uvicorn workers for your CPU count, and put Nginx in front.
- Consider async views only for I/O-heavy endpoints; do not rewrite everything to async by default.
For deployment details, see deploying Django on AWS.
A repeatable performance workflow
- Identify the slowest, most used endpoints from metrics.
- Profile them to see query counts and timings.
- Fix N+1 queries and over-fetching.
- Add indexes backed by
EXPLAIN ANALYZE. - Move slow work to Celery.
- Cache stable reads.
- Re-measure and repeat.
Keep a before-and-after record. It is useful both for engineering and for showing stakeholders the improvement.
When performance problems point to a bigger issue
Sometimes slowness is a symptom of an outdated stack or a design that no longer fits. Upgrading an old Django version often brings real performance gains, as covered in the Django upgrade playbook. In one legacy modernization project I migrated an enterprise app from Django 2.x to 5.x and optimized its query layer; you can read about it in the case study. For growing products, also review your SaaS architecture.
Frequently asked questions
Why is my Django app slow?
The most common causes are N+1 database queries, missing indexes, fetching too much data, doing slow work inside requests and a lack of caching. Profiling with Django Debug Toolbar or an APM tool will show which one applies.
How do I fix N+1 queries in Django?
Use select_related for foreign key and one-to-one relations and prefetch_related for many-to-many and reverse relations, applied in your queryset or DRF viewset so related data loads in a few queries instead of one per row.
How do I speed up Django REST Framework APIs?
Optimize the queryset with select_related/prefetch_related, paginate responses, return only needed fields from serializers, cache stable endpoints and move slow tasks to Celery.
Do I need to scale my servers to fix Django performance?
Usually not at first. Query optimization, indexing and caching often give larger gains than adding servers. Scale hardware after the application is efficient.
Can you audit and speed up my Django application?
Yes. A performance audit profiles your endpoints, finds the biggest bottlenecks and delivers prioritized fixes. If you want help, see my Django development services or read how to hire a Django developer.
Need a faster Django backend?
If your Django app or DRF API is slow, unstable under load or running on an outdated version, send me a message. Share your stack, rough traffic and the slowest endpoints, and I will tell you what is likely wrong and what it would take to fix it.