Skip to main content

Performance

Quick reference for optimizing Django application performance, database queries, and deployment.

🚀 Database Query Optimization

Query Analysis

# Enable query logging in development
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}

# Use django-debug-toolbar
INSTALLED_APPS = [
'debug_toolbar',
]

MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
]

# Query counting in code
from django.db import connection
from django.test.utils import override_settings

@override_settings(DEBUG=True)
def analyze_queries():
initial_queries = len(connection.queries)

# Your code here
posts = Post.objects.all()
for post in posts:
print(post.author.name) # N+1 problem!

print(f"Queries executed: {len(connection.queries) - initial_queries}")

Efficient Querying

# Bad: N+1 queries
posts = Post.objects.all()
for post in posts:
print(post.author.name) # Hits database for each author

# Good: Use select_related for ForeignKey/OneToOne
posts = Post.objects.select_related('author', 'category').all()
for post in posts:
print(post.author.name) # No additional queries

# Bad: N+1 for ManyToMany
for post in posts:
print([tag.name for tag in post.tags.all()])

# Good: Use prefetch_related for ManyToMany/reverse ForeignKey
posts = Post.objects.prefetch_related('tags', 'comments').all()
for post in posts:
print([tag.name for tag in post.tags.all()])

# Complex prefetch with custom queryset
from django.db.models import Prefetch

posts = Post.objects.prefetch_related(
Prefetch(
'comments',
queryset=Comment.objects.filter(is_approved=True).select_related('author')
)
).all()

Field Selection

# Only fetch needed fields
posts = Post.objects.only('title', 'slug', 'published_date').all()

# Exclude heavy fields
posts = Post.objects.defer('content', 'description').all()

# Values for simple data
post_data = Post.objects.values('title', 'author__name', 'published_date')

# Values list for even simpler data
post_titles = Post.objects.values_list('title', flat=True)
title_author_pairs = Post.objects.values_list('title', 'author__name')

Bulk Operations

# Bulk create (avoid individual saves)
posts = [
Post(title=f'Post {i}', content=f'Content {i}')
for i in range(1000)
]
Post.objects.bulk_create(posts, batch_size=100)

# Bulk update
posts = Post.objects.filter(is_published=False)
for post in posts:
post.view_count += 1
Post.objects.bulk_update(posts, ['view_count'], batch_size=100)

# Bulk update with single query
Post.objects.filter(category='tech').update(is_featured=True)

# Efficient counting
# Bad
total_posts = len(Post.objects.all())

# Good
total_posts = Post.objects.count()

# Conditional counting
published_count = Post.objects.filter(is_published=True).count()

🗄️ Database Optimization

Indexes

class Post(models.Model):
title = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(unique=True) # Automatically indexed
published_date = models.DateTimeField(db_index=True)
author = models.ForeignKey(User, on_delete=models.CASCADE) # Auto-indexed

class Meta:
indexes = [
# Composite indexes
models.Index(fields=['author', 'published_date']),
models.Index(fields=['category', '-published_date']),

# Partial indexes (PostgreSQL)
models.Index(
fields=['published_date'],
condition=models.Q(is_published=True),
name='published_posts_date_idx'
),

# Functional indexes (PostgreSQL)
models.Index(
models.functions.Lower('title'),
name='title_lower_idx'
),
]

Database Connection Optimization

# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'user',
'PASSWORD': 'pass',
'HOST': 'localhost',
'PORT': '5432',
'OPTIONS': {
'MAX_CONNS': 20,
},
'CONN_MAX_AGE': 60, # Persistent connections
}
}

# Connection pooling with django-db-pool
DATABASES['default']['ENGINE'] = 'django_db_pool.backends.postgresql'
DATABASES['default']['POOL_OPTIONS'] = {
'POOL_SIZE': 10,
'MAX_OVERFLOW': 10,
'RECYCLE': 24 * 60 * 60, # 24 hours
}

Raw SQL for Complex Queries

from django.db import connection

def complex_query():
with connection.cursor() as cursor:
cursor.execute("""
SELECT p.title, a.username, COUNT(c.id) as comment_count
FROM blog_post p
JOIN auth_user a ON p.author_id = a.id
LEFT JOIN blog_comment c ON p.id = c.post_id
WHERE p.published_date > %s
GROUP BY p.id, a.username
ORDER BY comment_count DESC
""", [timezone.now() - timedelta(days=30)])

return cursor.fetchall()

# Or use raw() for model instances
posts = Post.objects.raw(
"SELECT * FROM blog_post WHERE view_count > %s",
[1000]
)

💾 Caching Strategies

Multi-Level Caching

# settings.py
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
},
'sessions': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/2',
},
'pages': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/3',
}
}

Smart Caching Patterns

from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
import hashlib

class CachedPostManager(models.Manager):
def get_popular_posts(self, limit=10):
cache_key = f'popular_posts_{limit}'
posts = cache.get(cache_key)

if posts is None:
posts = list(
self.filter(is_published=True)
.order_by('-view_count')[:limit]
.select_related('author')
)
cache.set(cache_key, posts, 300) # 5 minutes

return posts

def get_posts_by_tag(self, tag_slug):
cache_key = f'posts_tag_{tag_slug}'
posts = cache.get(cache_key)

if posts is None:
posts = list(
self.filter(tags__slug=tag_slug, is_published=True)
.select_related('author')
.prefetch_related('tags')
)
cache.set(cache_key, posts, 600) # 10 minutes

return posts

# Cache invalidation
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver

@receiver([post_save, post_delete], sender=Post)
def invalidate_post_cache(sender, instance, **kwargs):
# Clear related cache keys
cache.delete_pattern('popular_posts_*')
cache.delete_pattern('posts_tag_*')
cache.delete(f'post_{instance.id}')

View-Level Caching

from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from django.utils.decorators import method_decorator

# Simple page caching
@cache_page(60 * 15) # 15 minutes
def post_list(request):
posts = Post.objects.published().select_related('author')
return render(request, 'posts.html', {'posts': posts})

# Vary cache by headers
@cache_page(60 * 30)
@vary_on_headers('User-Agent', 'Accept-Language')
def responsive_view(request):
return render(request, 'responsive.html')

# Conditional caching
def cached_post_detail(request, slug):
post = get_object_or_404(Post, slug=slug)

# Cache key based on post and user
cache_key = f'post_detail_{slug}_{request.user.id or "anon"}'
response = cache.get(cache_key)

if response is None:
response = render(request, 'post_detail.html', {'post': post})
cache.set(cache_key, response, 900) # 15 minutes

return response

# Class-based view caching
@method_decorator(cache_page(60 * 10), name='dispatch')
class PostListView(ListView):
model = Post
queryset = Post.objects.published().select_related('author')

🏗️ Static Files & CDN

Static Files Optimization

# settings.py
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# Compression
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# CDN for static files
if not DEBUG:
STATIC_URL = 'https://cdn.example.com/static/'

# Media files CDN
MEDIA_URL = 'https://media.example.com/media/'

# WhiteNoise for serving static files
MIDDLEWARE = [
'whitenoise.middleware.WhiteNoiseMiddleware',
]

WHITENOISE_USE_FINDERS = True
WHITENOISE_AUTOREFRESH = True

Asset Pipeline

# Compressor settings
INSTALLED_APPS = [
'compressor',
]

COMPRESS_ENABLED = True
COMPRESS_CSS_FILTERS = [
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.rCSSMinFilter',
]
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter',
]

# Template usage
"""
{% load compress %}

{% compress css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/mobile.css' %}">
{% endcompress %}

{% compress js %}
<script src="{% static 'js/jquery.js' %}"></script>
<script src="{% static 'js/app.js' %}"></script>
{% endcompress %}
"""

🔧 Application Performance

Code Optimization

# Use generators for large datasets
def process_large_dataset():
# Bad: Loads all into memory
posts = Post.objects.all()
for post in posts:
process_post(post)

# Good: Use iterator
for post in Post.objects.iterator(chunk_size=100):
process_post(post)

# Lazy evaluation
from django.utils.functional import lazy

def get_expensive_data():
# This won't be called until actually needed
return expensive_calculation()

lazy_data = lazy(get_expensive_data, str)

# Property caching
from functools import cached_property

class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()

@cached_property
def word_count(self):
# Expensive calculation cached on instance
return len(self.content.split())

@property
def reading_time(self):
return self.word_count // 200 # Uses cached word_count

Session Optimization

# Use database sessions for multiple servers
SESSION_ENGINE = 'django.contrib.sessions.backends.db'

# Or cache-based sessions for speed
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'sessions'

# Reduce session data
def minimal_session_view(request):
# Store only essential data
request.session['user_id'] = request.user.id
# Don't store large objects in sessions

📊 Monitoring & Profiling

Performance Monitoring

# Django Debug Toolbar
INSTALLED_APPS = [
'debug_toolbar',
]

MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
]

# django-silk for profiling
INSTALLED_APPS = [
'silk',
]

MIDDLEWARE = [
'silk.middleware.SilkyMiddleware',
]

# Custom performance middleware
import time
import logging

performance_logger = logging.getLogger('performance')

class PerformanceMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
start_time = time.time()

response = self.get_response(request)

duration = time.time() - start_time
if duration > 1.0: # Log slow requests
performance_logger.warning(
f'Slow request: {request.path} took {duration:.2f}s'
)

response['X-Response-Time'] = f'{duration:.3f}s'
return response

Database Query Monitoring

from django.db import connection
from django.conf import settings

class QueryCountMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
if settings.DEBUG:
initial_queries = len(connection.queries)

response = self.get_response(request)

if settings.DEBUG:
query_count = len(connection.queries) - initial_queries
if query_count > 10: # Threshold
print(f'WARNING: {request.path} made {query_count} queries')

return response

🚀 Production Optimization

WSGI/ASGI Configuration

# gunicorn settings
bind = "0.0.0.0:8000"
workers = 2 * cpu_count() + 1 # General rule
worker_class = "sync" # or "gevent" for async
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 100
preload_app = True
timeout = 30

# For async Django
worker_class = "uvicorn.workers.UvicornWorker"

Production Settings

# settings/production.py
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# Database connection pooling
DATABASES['default']['CONN_MAX_AGE'] = 60

# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/var/log/django/django.log',
'maxBytes': 1024*1024*5, # 5 MB
'backupCount': 5,
'formatter': 'verbose',
},
},
'root': {
'handlers': ['file'],
'level': 'INFO',
},
}

# Security headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'

This cheatsheet covers essential Django performance optimization techniques for building scalable web applications.