Python/Django Performance: Server-Side Speed Guide
Django powers Instagram and Pinterest. Learn the performance patterns they use to keep Python-based web apps fast at scale.
Python isn't the fastest language, but Django powers some of the world's highest-traffic sites — Instagram, Pinterest, Disqus, and Mozilla. The secret isn't Python speed; it's architecture and optimization patterns.
Django Performance Fundamentals
1. Database Query Optimization
The ORM makes it easy to accidentally create N+1 queries:
# BAD: N+1 queries
posts = Post.objects.all()
for post in posts:
print(post.author.name) # 1 query per post
# GOOD: select_related (JOIN)
posts = Post.objects.select_related('author').all()
for post in posts:
print(post.author.name) # 0 additional queries
# GOOD: prefetch_related (for M2M relationships)
posts = Post.objects.prefetch_related('tags').all()
2. Use .only() and .defer()
# BAD: fetches all 20 columns including large text fields
posts = Post.objects.all()
# GOOD: only needed columns
posts = Post.objects.only('id', 'title', 'slug', 'published_at')
# Or defer heavy columns
posts = Post.objects.defer('content', 'raw_html')
3. Add Database Indexes
class Post(models.Model):
slug = models.SlugField(db_index=True)
published_at = models.DateTimeField(db_index=True)
class Meta:
indexes = [
models.Index(fields=['status', 'published_at']),
models.Index(fields=['author', '-published_at']),
]
Caching Strategies
View-Level Caching
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # Cache for 15 minutes
def blog_index(request):
posts = Post.objects.filter(published=True).order_by('-published_at')
return render(request, 'blog/index.html', {'posts': posts})
Template Fragment Caching
{% load cache %}
{% cache 3600 sidebar %}
<div class="sidebar">
{% for category in categories %}
<a href="{{ category.url }}">{{ category.name }}</a>
{% endfor %}
</div>
{% endcache %}
Low-Level Caching
from django.core.cache import cache
def get_popular_posts():
key = 'popular_posts'
posts = cache.get(key)
if posts is None:
posts = Post.objects.filter(
views__gt=1000
).order_by('-views')[:10]
cache.set(key, posts, 3600)
return posts
Use Redis as Cache Backend
# settings.py
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379',
}
}
ASGI and Async Views
Django 4.1+ supports async views for better concurrency:
import httpx
from django.http import JsonResponse
async def api_proxy(request):
async with httpx.AsyncClient() as client:
response = await client.get('https://api.example.com/data')
return JsonResponse(response.json())
Use Uvicorn or Daphne as your ASGI server:
uvicorn myproject.asgi:application --workers 4
Static File Optimization
WhiteNoise
Serve static files efficiently without nginx:
# settings.py
MIDDLEWARE = [
'whitenoise.middleware.WhiteNoiseMiddleware',
# ...
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WhiteNoise automatically:
- Adds cache headers (immutable, max-age=1y for hashed files)
- Serves Brotli/Gzip compressed files
- Generates versioned filenames
Production Checklist
# settings.py (production)
DEBUG = False
CONN_MAX_AGE = 600 # Persistent DB connections
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# Use connection pooling
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'CONN_MAX_AGE': 600,
'CONN_HEALTH_CHECKS': True,
}
}
Performance Results
| Optimization | Requests/sec |
|---|---|
| Default Django | 100-300 |
| + Gunicorn (4 workers) | 400-1200 |
| + Redis caching | 2000-5000 |
| + Full-page caching | 5000-15000 |
| + CDN | 50000+ (from cache) |
Monitor Your Django App
Django apps need monitoring as data and traffic grow. BadPageSpeed tracks your frontend Core Web Vitals.
Ready to stop wasting ad spend?
Track your landing page performance, monitor Core Web Vitals, and calculate exactly how much slow pages cost you.
Start Free — No Credit Card