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

  1. 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).
  2. Template fragment{% cache 600 sidebar %} caches just the expensive part of a page.
  3. Low-level cache APIcache.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

Example · python
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.

Example · python
# 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.

Example · python
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.

Example · python
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

  • Be the first to comment on this lesson.