Why and Where to Cache
Caching stores the result of an expensive operation, like a complex database query or a rendered template, so subsequent requests can reuse it instead of recomputing it from scratch. Django's caching framework supports multiple granularities: whole-site caching via UpdateCacheMiddleware and FetchFromCacheMiddleware, per-view caching with the @cache_page decorator, template-fragment caching with the {% cache %} tag, and the low-level cache API (cache.get, cache.set) for caching arbitrary Python objects like query results.
Cricket analogy: Caching is like a commentator keeping a running tally of a batsman's strike rate on a notepad rather than recalculating it from every single ball bowled since the start of the innings each time someone asks.
The Low-Level Cache API
The low-level API, accessed via django.core.cache.cache, gives fine-grained control with methods like cache.set(key, value, timeout), cache.get(key, default=None), cache.delete(key), and cache.get_or_set(key, callable, timeout), which is especially useful for wrapping an expensive function so it only executes on a cache miss. Cache keys should be deliberately namespaced (like f'product_detail_{product_id}_v2') to avoid collisions and to support easy invalidation when the underlying data changes, such as deleting the key inside a post_save signal receiver.
Cricket analogy: cache.get_or_set is like a scorer checking a printed scorecard first before recalculating a batsman's average from scratch, only doing the recalculation if the scorecard is missing or outdated.
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from .models import Product
def get_product_detail(product_id):
cache_key = f'product_detail_{product_id}_v1'
def compute():
product = Product.objects.select_related('category').get(pk=product_id)
return {
'name': product.name,
'price': str(product.price),
'category': product.category.name,
}
return cache.get_or_set(cache_key, compute, timeout=60 * 15)
@cache_page(60 * 5)
def product_list_view(request):
products = Product.objects.select_related('category').all()
...
# Invalidate on change via a signal receiver
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Product)
def invalidate_product_cache(sender, instance, **kwargs):
cache.delete(f'product_detail_{instance.pk}_v1')CACHES['default']['BACKEND'] set to django_redis.cache.RedisCache (via django-redis) is the most common production choice because Redis supports atomic operations, TTL expiry, and can be shared across multiple app server processes — unlike LocMemCache, which is per-process and won't stay consistent across a multi-worker Gunicorn deployment.
Template Fragment Caching and Invalidation Strategy
The {% load cache %} and {% cache timeout fragment_name arg1 arg2 %} template tags let you cache just a portion of a rendered template, like a sidebar of trending posts, rather than the whole page, which is useful when most of a page is dynamic but one section is expensive and rarely changes. The hardest part of caching is invalidation: a common pattern is versioned cache keys (bumping a suffix like _v2 on deploy) or explicit cache.delete() calls triggered from post_save/post_delete signals, since Django's cache backends generally don't support wildcard deletion by pattern without backend-specific extensions.
Cricket analogy: Template fragment caching is like a broadcast graphic caching only the 'top run scorers this series' panel while the live scoreboard ticker updates in real time around it.
Cached data that isn't invalidated on write becomes stale silently — there's no error, just outdated content served to users. Always pair a cache.set() with a clear invalidation trigger (a signal, a save() override, or a short timeout) rather than relying on a long timeout alone to eventually self-correct.
- Django supports multiple caching granularities: whole-site, per-view, template-fragment, and low-level API.
- cache.get_or_set() is a convenient pattern to compute a value only on a cache miss.
- Cache keys should be deliberately namespaced and versioned to avoid collisions and simplify invalidation.
- Redis (via django-redis) is the standard production backend because it's shared across worker processes.
- LocMemCache is per-process and inconsistent across multi-worker deployments — dev/test only.
- {% cache %} template tags cache rendered fragments, not the whole response, for partially dynamic pages.
- Invalidation is the hardest part of caching; pair writes with explicit cache.delete() or signal-driven invalidation.
Practice what you learned
1. What does cache.get_or_set(key, callable, timeout) do?
2. Why is LocMemCache generally unsuitable for a production deployment with multiple Gunicorn worker processes?
3. What is generally considered the hardest part of implementing a caching strategy?
4. What does the {% cache %} template tag cache, compared to @cache_page?
5. Why would you version a cache key, such as using product_detail_123_v2 instead of product_detail_123?
Was this page helpful?
You May Also Like
Django Middleware
Understand how Django middleware intercepts every request and response to implement cross-cutting concerns like authentication, security headers, and logging.
Django Signals
Learn how Django's signal dispatcher lets decoupled parts of an application react to model and request lifecycle events like save, delete, and request_finished.
Building APIs with Django REST Framework
Learn how Django REST Framework turns Django models and views into robust, browsable JSON APIs using serializers, viewsets, and routers.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics