View-level caching is one of the most effective ways to improve Django application performance by caching entire HTTP responses. This approach eliminates the need to execute view logic, database queries, and template rendering for cached responses, providing dramatic performance improvements for content that doesn't change frequently.
# views.py
from django.views.decorators.cache import cache_page
from django.shortcuts import render
from django.http import JsonResponse
from .models import Post, Category
@cache_page(60 * 15) # Cache for 15 minutes
def blog_list(request):
"""Cache the entire blog list view."""
posts = Post.objects.filter(published=True).order_by('-created_at')[:10]
categories = Category.objects.all()
context = {
'posts': posts,
'categories': categories,
}
return render(request, 'blog/list.html', context)
@cache_page(60 * 60) # Cache for 1 hour
def about_page(request):
"""Cache static about page."""
return render(request, 'pages/about.html')
# API view caching
@cache_page(60 * 5) # Cache for 5 minutes
def api_posts(request):
"""Cache API response."""
posts = Post.objects.filter(published=True).values(
'id', 'title', 'slug', 'created_at'
)
return JsonResponse({'posts': list(posts)})
# views.py
from django.views.generic import ListView, DetailView
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from django.views.decorators.vary import vary_on_headers
@method_decorator(cache_page(60 * 15), name='dispatch')
class PostListView(ListView):
"""Cached list view."""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(published=True).select_related('author')
@method_decorator([
cache_page(60 * 30),
vary_on_headers('User-Agent', 'Accept-Language')
], name='dispatch')
class PostDetailView(DetailView):
"""Cached detail view with vary headers."""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.filter(published=True).select_related('author')
# views.py
from django.core.cache import cache
from django.views.decorators.cache import cache_control
from django.utils.cache import get_cache_key
import hashlib
def conditional_cache_view(request):
"""Cache view based on specific conditions."""
# Generate cache key based on request parameters
cache_key_parts = [
'blog_list',
request.GET.get('category', 'all'),
request.GET.get('page', '1'),
request.GET.get('sort', 'date'),
]
cache_key = ':'.join(cache_key_parts)
# Try to get cached response
cached_response = cache.get(cache_key)
if cached_response:
return cached_response
# Generate response
category = request.GET.get('category')
page = int(request.GET.get('page', 1))
sort_by = request.GET.get('sort', 'date')
posts = Post.objects.filter(published=True)
if category and category != 'all':
posts = posts.filter(category__slug=category)
if sort_by == 'title':
posts = posts.order_by('title')
else:
posts = posts.order_by('-created_at')
# Pagination
from django.core.paginator import Paginator
paginator = Paginator(posts, 10)
posts_page = paginator.get_page(page)
context = {
'posts': posts_page,
'current_category': category,
'current_sort': sort_by,
}
response = render(request, 'blog/list.html', context)
# Cache the response for 10 minutes
cache.set(cache_key, response, 600)
return response
# views.py
from django.contrib.auth.decorators import login_required
from django.core.cache import cache
@login_required
def user_dashboard(request):
"""Cache user-specific dashboard."""
user_id = request.user.id
cache_key = f'dashboard_user_{user_id}'
# Check cache first
cached_content = cache.get(cache_key)
if cached_content:
return cached_content
# Generate user-specific content
user_posts = Post.objects.filter(author=request.user)
user_stats = {
'total_posts': user_posts.count(),
'published_posts': user_posts.filter(published=True).count(),
'draft_posts': user_posts.filter(published=False).count(),
}
context = {
'user_posts': user_posts[:5], # Recent 5 posts
'user_stats': user_stats,
}
response = render(request, 'dashboard/user.html', context)
# Cache for 5 minutes (shorter for user-specific content)
cache.set(cache_key, response, 300)
return response
# Vary cache by user
from django.views.decorators.vary import vary_on_cookie
@vary_on_cookie
@cache_page(60 * 10)
def personalized_content(request):
"""Cache varies by user cookies."""
# This will create separate cache entries for different users
user_preferences = request.COOKIES.get('preferences', 'default')
context = {
'content': get_personalized_content(user_preferences),
}
return render(request, 'personalized.html', context)
# utils/cache_keys.py
import hashlib
from django.utils.cache import get_cache_key
from django.core.cache.utils import make_template_fragment_key
class CacheKeyGenerator:
"""Generate consistent cache keys for views."""
@staticmethod
def view_cache_key(view_name, **kwargs):
"""Generate cache key for view with parameters."""
key_parts = [view_name]
# Add sorted parameters
for key, value in sorted(kwargs.items()):
key_parts.append(f"{key}={value}")
return ':'.join(key_parts)
@staticmethod
def user_view_cache_key(view_name, user_id, **kwargs):
"""Generate user-specific cache key."""
key_parts = [view_name, f"user_{user_id}"]
for key, value in sorted(kwargs.items()):
key_parts.append(f"{key}={value}")
return ':'.join(key_parts)
@staticmethod
def paginated_cache_key(view_name, page, per_page=10, **filters):
"""Generate cache key for paginated views."""
key_parts = [view_name, f"page_{page}", f"per_page_{per_page}"]
# Add filters
for key, value in sorted(filters.items()):
if value: # Only include non-empty filters
key_parts.append(f"{key}={value}")
return ':'.join(key_parts)
@staticmethod
def hash_cache_key(base_key, max_length=250):
"""Hash long cache keys to fit within limits."""
if len(base_key) <= max_length:
return base_key
# Keep readable prefix and hash the rest
prefix = base_key[:50]
suffix_hash = hashlib.md5(base_key.encode()).hexdigest()
return f"{prefix}:{suffix_hash}"
# Usage in views
def advanced_cached_view(request):
"""View with advanced cache key generation."""
filters = {
'category': request.GET.get('category'),
'tag': request.GET.get('tag'),
'author': request.GET.get('author'),
}
page = int(request.GET.get('page', 1))
cache_key = CacheKeyGenerator.paginated_cache_key(
'blog_list',
page=page,
**filters
)
# Hash if too long
cache_key = CacheKeyGenerator.hash_cache_key(cache_key)
cached_response = cache.get(cache_key)
if cached_response:
return cached_response
# Generate response...
response = generate_response(request, filters, page)
# Cache with appropriate timeout
timeout = 600 if any(filters.values()) else 1800 # Shorter for filtered results
cache.set(cache_key, response, timeout)
return response
# utils/cache_versioning.py
from django.core.cache import cache
from django.conf import settings
class VersionedCache:
"""Cache with version support for easy invalidation."""
def __init__(self, cache_alias='default'):
self.cache = cache
self.version_key_prefix = 'cache_version'
def get_version(self, namespace):
"""Get current version for a namespace."""
version_key = f"{self.version_key_prefix}:{namespace}"
version = self.cache.get(version_key)
if version is None:
version = 1
self.cache.set(version_key, version, None) # Never expires
return version
def increment_version(self, namespace):
"""Increment version to invalidate all cached items in namespace."""
version_key = f"{self.version_key_prefix}:{namespace}"
try:
self.cache.incr(version_key)
except ValueError:
# Key doesn't exist, set to 1
self.cache.set(version_key, 1, None)
def versioned_key(self, key, namespace):
"""Generate versioned cache key."""
version = self.get_version(namespace)
return f"{key}:v{version}"
def get(self, key, namespace, default=None):
"""Get value with versioned key."""
versioned_key = self.versioned_key(key, namespace)
return self.cache.get(versioned_key, default)
def set(self, key, value, namespace, timeout=None):
"""Set value with versioned key."""
versioned_key = self.versioned_key(key, namespace)
return self.cache.set(versioned_key, value, timeout)
def delete(self, key, namespace):
"""Delete specific versioned key."""
versioned_key = self.versioned_key(key, namespace)
return self.cache.delete(versioned_key)
def invalidate_namespace(self, namespace):
"""Invalidate entire namespace by incrementing version."""
self.increment_version(namespace)
# Usage in views
versioned_cache = VersionedCache()
def versioned_cached_view(request, category_slug):
"""View with versioned caching."""
cache_key = f"category_posts:{category_slug}"
namespace = f"category:{category_slug}"
# Try versioned cache
cached_response = versioned_cache.get(cache_key, namespace)
if cached_response:
return cached_response
# Generate response
category = get_object_or_404(Category, slug=category_slug)
posts = Post.objects.filter(category=category, published=True)
context = {
'category': category,
'posts': posts,
}
response = render(request, 'blog/category.html', context)
# Cache with version
versioned_cache.set(cache_key, response, namespace, 1800)
return response
# Invalidate when category is updated
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Category)
def invalidate_category_cache(sender, instance, **kwargs):
"""Invalidate category cache when updated."""
namespace = f"category:{instance.slug}"
versioned_cache.invalidate_namespace(namespace)
# signals.py
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from django.core.cache import cache
from .models import Post, Category, Tag
@receiver(post_save, sender=Post)
def invalidate_post_cache(sender, instance, created, **kwargs):
"""Invalidate caches when post is saved."""
cache_keys_to_delete = [
'blog_list:all:1', # First page of all posts
'blog_list:all:2', # Second page might be affected
f'category_posts:{instance.category.slug}',
'recent_posts',
'popular_posts',
]
# If post is published, invalidate more caches
if instance.published:
cache_keys_to_delete.extend([
'published_posts_count',
'sitemap_posts',
'rss_feed',
])
# Delete cache keys
cache.delete_many(cache_keys_to_delete)
# Also delete post-specific cache
cache.delete(f'post_detail:{instance.slug}')
@receiver(post_delete, sender=Post)
def invalidate_post_cache_on_delete(sender, instance, **kwargs):
"""Invalidate caches when post is deleted."""
# Similar to post_save but for deletion
invalidate_post_cache(sender, instance, False, **kwargs)
@receiver(m2m_changed, sender=Post.tags.through)
def invalidate_tag_cache(sender, instance, action, pk_set, **kwargs):
"""Invalidate tag-related caches when post tags change."""
if action in ['post_add', 'post_remove', 'post_clear']:
# Invalidate tag pages
if pk_set:
for tag_id in pk_set:
try:
tag = Tag.objects.get(id=tag_id)
cache.delete(f'tag_posts:{tag.slug}')
except Tag.DoesNotExist:
pass
# Invalidate post detail cache (tags changed)
cache.delete(f'post_detail:{instance.slug}')
class CacheInvalidator:
"""Centralized cache invalidation logic."""
@staticmethod
def invalidate_post_related(post):
"""Invalidate all caches related to a post."""
cache_keys = [
# List views
'blog_list:all:1',
'blog_list:all:2',
f'blog_list:category_{post.category.slug}:1',
f'blog_list:author_{post.author.id}:1',
# Detail view
f'post_detail:{post.slug}',
# Aggregate views
'recent_posts',
'popular_posts',
'featured_posts',
# Counts and stats
'published_posts_count',
f'author_posts_count:{post.author.id}',
f'category_posts_count:{post.category.slug}',
]
# Add tag-related caches
for tag in post.tags.all():
cache_keys.append(f'tag_posts:{tag.slug}')
cache.delete_many(cache_keys)
@staticmethod
def invalidate_category_related(category):
"""Invalidate all caches related to a category."""
cache_keys = [
f'category_posts:{category.slug}',
f'category_detail:{category.slug}',
'all_categories',
'category_tree',
]
cache.delete_many(cache_keys)
@staticmethod
def invalidate_user_related(user):
"""Invalidate all caches related to a user."""
cache_keys = [
f'user_profile:{user.id}',
f'user_posts:{user.id}',
f'author_posts:{user.id}',
f'dashboard_user_{user.id}',
]
cache.delete_many(cache_keys)
# utils/cache_refresh.py
from django.core.cache import cache
from django.utils import timezone
from datetime import timedelta
import threading
class RefreshAheadCache:
"""Cache that refreshes content before expiration."""
def __init__(self, refresh_threshold=0.8):
self.refresh_threshold = refresh_threshold
def get_or_refresh(self, key, refresh_func, timeout=3600):
"""Get cached value or refresh if near expiration."""
# Try to get cached value with metadata
cache_data = cache.get(f"{key}:data")
cache_meta = cache.get(f"{key}:meta")
if cache_data is not None and cache_meta is not None:
# Check if we need to refresh
created_at = cache_meta['created_at']
age = (timezone.now() - created_at).total_seconds()
if age > (timeout * self.refresh_threshold):
# Refresh in background
threading.Thread(
target=self._background_refresh,
args=(key, refresh_func, timeout)
).start()
return cache_data
# Cache miss - refresh synchronously
return self._refresh_cache(key, refresh_func, timeout)
def _refresh_cache(self, key, refresh_func, timeout):
"""Refresh cache synchronously."""
try:
new_data = refresh_func()
# Store data and metadata
cache.set(f"{key}:data", new_data, timeout)
cache.set(f"{key}:meta", {
'created_at': timezone.now(),
'timeout': timeout
}, timeout)
return new_data
except Exception as e:
# Log error and return None
import logging
logger = logging.getLogger(__name__)
logger.error(f"Cache refresh failed for {key}: {e}")
return None
def _background_refresh(self, key, refresh_func, timeout):
"""Refresh cache in background thread."""
self._refresh_cache(key, refresh_func, timeout)
# Usage in views
refresh_cache = RefreshAheadCache()
def auto_refreshing_view(request):
"""View with automatic cache refresh."""
cache_key = 'expensive_blog_data'
def refresh_function():
# Expensive operation
return {
'posts': list(Post.objects.filter(published=True).values()),
'categories': list(Category.objects.values()),
'stats': calculate_blog_stats(),
}
# Get data with automatic refresh
data = refresh_cache.get_or_refresh(
cache_key,
refresh_function,
timeout=1800 # 30 minutes
)
if data is None:
# Fallback if refresh fails
data = {'posts': [], 'categories': [], 'stats': {}}
return render(request, 'blog/dashboard.html', data)
# middleware/cache_monitoring.py
import time
from django.core.cache import cache
from django.utils.deprecation import MiddlewareMixin
import logging
logger = logging.getLogger('cache_performance')
class CacheMonitoringMiddleware(MiddlewareMixin):
"""Monitor cache performance for views."""
def process_request(self, request):
request._cache_start_time = time.time()
request._cache_hits = 0
request._cache_misses = 0
def process_response(self, request, response):
if hasattr(request, '_cache_start_time'):
duration = time.time() - request._cache_start_time
# Log cache performance
cache_hits = getattr(request, '_cache_hits', 0)
cache_misses = getattr(request, '_cache_misses', 0)
total_cache_ops = cache_hits + cache_misses
if total_cache_ops > 0:
hit_rate = (cache_hits / total_cache_ops) * 100
logger.info(
f"Cache performance - Path: {request.path}, "
f"Duration: {duration:.3f}s, "
f"Hit rate: {hit_rate:.1f}%, "
f"Hits: {cache_hits}, Misses: {cache_misses}"
)
return response
# Decorator to track cache operations
def track_cache_operation(operation_type):
"""Decorator to track cache hits/misses."""
def decorator(func):
def wrapper(request, *args, **kwargs):
result = func(request, *args, **kwargs)
# Track the operation
if hasattr(request, f'_cache_{operation_type}s'):
current_count = getattr(request, f'_cache_{operation_type}s')
setattr(request, f'_cache_{operation_type}s', current_count + 1)
return result
return wrapper
return decorator
# Usage in views
@track_cache_operation('hit')
def cached_view_with_tracking(request):
"""View that tracks cache hits."""
cache_key = 'tracked_view_data'
cached_data = cache.get(cache_key)
if cached_data:
# This is a cache hit
return render(request, 'template.html', cached_data)
# This would be a cache miss
# Mark as miss and generate data
if hasattr(request, '_cache_misses'):
request._cache_misses += 1
data = generate_expensive_data()
cache.set(cache_key, data, 600)
return render(request, 'template.html', data)
Per-view caching provides excellent performance improvements with minimal code changes. The key is choosing appropriate cache timeouts, implementing proper invalidation strategies, and monitoring cache effectiveness. Start with simple time-based caching and gradually implement more sophisticated patterns like versioned caching and refresh-ahead strategies as your application's caching needs evolve.
Cache Backends
Django supports multiple cache backends, each with distinct characteristics, performance profiles, and use cases. Choosing the right backend and configuring it properly is crucial for optimal caching performance. This chapter covers all available backends, their configuration options, and guidance for selecting the best backend for your specific requirements.
Low Level Cache API
Django's low-level cache API provides fine-grained control over caching operations, enabling sophisticated caching strategies for specific data, computed results, and complex objects. This approach offers maximum flexibility for optimizing application performance through targeted caching of expensive operations, database queries, and external API calls.