Caching

Per View Caching

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.

Per View Caching

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.

Basic View Caching

Using the cache_page Decorator

# 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)})

Class-Based View Caching

# 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')

Conditional View Caching

Cache Based on Request Parameters

# 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

User-Specific Caching

# 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)

Advanced Cache Key Generation

Custom Cache Key Functions

# 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

Cache Versioning

# 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)

Cache Invalidation Strategies

Signal-Based Invalidation

# 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)

Time-Based Invalidation with Refresh

# 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)

Performance Monitoring

Cache Hit Rate Tracking

# 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.