Caching Strategies
Layer per-view, template-fragment and low-level caching — and master the hard part: invalidation.
Caching is the highest-leverage performance work you can do, and the easiest to get subtly wrong. Django gives you a tiered toolkit; the skill is applying the smallest cache that solves the problem and having a clear invalidation story.
The levels, coarse to fine
- Per-site / per-view — cache an entire response. Great for anonymous, read-mostly pages; dangerous for anything personalised (you can serve one user's page to another).
- Template fragment —
{% cache 600 sidebar %}caches just the expensive part of a page. - Low-level cache API —
cache.get_or_set(key, fn, timeout)caches exactly one expensive value. This is the one you'll use most in mature code.
from django.core.cache import cache
def expensive_stats():
return cache.get_or_set(
'dashboard:stats',
lambda: compute_stats(), # only runs on a miss
timeout=300,
)Invalidation — the actually-hard part
Two strategies, and you should know both. Time-based (TTL) is simple and eventually consistent — good enough for most reads. Key-based / event-based invalidation deletes or versions the key when the underlying data changes (often from a post_save signal), giving immediate freshness at the cost of more moving parts. Use a shared backend like Redis in production so every worker sees the same cache.
Example
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie
from django.utils.decorators import method_decorator
from functools import wraps
# 1) Low-level get_or_set with a versioned key — self-invalidating.
def get_article_html(article):
key = f'article:html:{article.pk}:{int(article.updated_at.timestamp())}'
return cache.get_or_set(key, lambda: render_markdown(article.body), timeout=86400)
# 2) A cache-aside helper with stampede protection via a short lock.
def cached_expensive(key, producer, timeout=300, lock_timeout=10):
value = cache.get(key)
if value is not None:
return value
lock_key = f'{key}:lock'
if cache.add(lock_key, '1', lock_timeout): # only one worker wins the lock
try:
value = producer()
cache.set(key, value, timeout)
finally:
cache.delete(lock_key)
return value
# Lost the race: serve slightly stale or recompute cheaply.
return cache.get(key) or producer()
# 3) Per-view cache that varies on the logged-in user's session cookie.
@method_decorator(cache_page(60 * 15), name='dispatch')
@method_decorator(vary_on_cookie, name='dispatch')
class PublicLeaderboardView(ListView):
model = Score
# 4) Event-based invalidation from a signal, when TTL isn't fresh enough.
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=SiteConfig)
def bust_config_cache(sender, **kwargs):
cache.delete('site:config')
# settings.py — a real shared backend, not per-process memory:
# CACHES = {'default': {
# 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
# 'LOCATION': 'redis://cache:6379/1',
# }}When to use it
- A developer caches the result of an expensive homepage queryset with cache.set() to serve it from Redis on repeat visits.
- A team uses the @cache_page decorator to cache an entire view's response for 15 minutes to reduce database load.
- A developer uses cache.get_or_set() to populate the cache with computed data only on a miss, avoiding stampedes.
More examples
Configure Redis cache backend
Configures Django to use Redis as the cache backend using the built-in Redis cache class.
# settings.py
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
}
}Cache a view with decorator
Caches the entire homepage response for 15 minutes; subsequent requests are served from cache.
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # 15 minutes
def homepage(request):
featured = Post.objects.filter(featured=True)[:5]
return render(request, "home.html", {"posts": featured})Low-level cache API
Uses the low-level cache API to store and retrieve a list of top posts, computing it only on a cache miss.
from django.core.cache import cache
def get_top_posts():
key = "top_posts"
posts = cache.get(key)
if posts is None:
posts = list(Post.objects.filter(draft=False).order_by("-view_count")[:10])
cache.set(key, posts, timeout=300) # 5 minutes
return posts
Discussion