After building 15+ Django SaaS products, I see the same five bottlenecks in every MVP that hits growth. Understanding these before they bite saves weeks of firefighting.
1. N+1 query problem (the #1 killer)
- A dashboard page that loads 50 projects, each with a team and latest activity, can generate 150+ database queries instead of 3. Response time: 2-5 seconds instead of 100ms.
- Detection: Install
django-debug-toolbar(development) anddjango-silk(production profiling). Any page with more than 10 queries is suspect. - Fix:
select_related()for ForeignKey/OneToOne joins,prefetch_related()for ManyToMany/reverse FK. Example:Project.objects.select_related("owner", "team").prefetch_related("tasks__assignee")— 3 queries instead of 150. - Impact: Typically reduces query count by 90% and page load time by 80%. Single biggest performance improvement in any Django application.
2. Missing database indexes
- Django creates indexes on primary keys and unique fields automatically, but not on fields you filter, order, or join by. A
WHERE status = 'active' AND tenant_id = 5on a 500,000-row table without a composite index takes 500ms+. - Fix: Add
class Meta: indexes = [models.Index(fields=["tenant", "status", "-created_at"])]. UseEXPLAIN ANALYZEin PostgreSQL to verify index usage. - Common missing indexes:
tenant_id+created_at(tenant-scoped time queries),status+assigned_to(filtered lists),email(user lookups), any field used inorder_by().
3. Synchronous heavy tasks in request cycle
- Sending emails, generating PDFs, processing uploads, calling external APIs — these should never block the HTTP response. A 3-second email send makes the entire request take 3+ seconds.
- Fix: Celery with Redis broker. Move all I/O-heavy operations to async tasks:
send_welcome_email.delay(user_id). Response time drops from 3 seconds to 200ms.
4. No caching layer
- Dashboard data that changes once per hour gets recalculated on every page load. With 100 concurrent users loading the dashboard, that is 100 identical expensive queries per minute.
- Fix: Redis cache with
django-redis. Cache dashboard aggregations, user permissions, plan features, and frequently accessed configuration. TTL: 5-60 minutes depending on data freshness requirements.
5. Monolithic media serving
- Django serving static files and user uploads through Gunicorn wastes application server resources. A 5MB file download ties up a Gunicorn worker for 2-10 seconds.
- Fix: Serve static files via CDN (Cloudflare, BunnyCDN at EUR 1/month per TB). Serve user uploads via S3 pre-signed URLs (files never touch your application server).