Caching

Django Intermediate/Advanced — Caching
Django Intermediate/Advanced
Course 2 · Chapter 6 · Caching

⚡ Caching

Chapter 5 offloaded work that was too slow to make a user wait for. This chapter is about work that's fast enough to run inline, but wasteful to redo on every single request when the result barely changes — an expensive book list query, a rendered page fragment that's identical for the next thousand visitors. Django's cache framework covers everything from one cached value to an entire cached page.

Cache Backends

Like DATABASES, CACHES in settings.py configures where cached data actually lives:

# settings.py CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", } }

Redis / Memcached

The real production choices — an in-memory store separate from Django itself, fast, and shared across every web server process.

LocMemCache

A simple in-process cache, fine for local development — but each server process gets its own separate cache, which doesn't reflect production behavior.

DummyCache

Implements the same API but never actually stores anything — every get() misses. Useful for tests where you deliberately want caching disabled.

🔑 The Low-Level Cache API

from django.core.cache import cache cache.set("featured_book_count", 42, timeout=300) # cache for 300 seconds cache.get("featured_book_count") # 42 — or None if expired/missing cache.delete("featured_book_count") # explicit invalidation

get_or_set() combines the check-then-compute-then-store pattern into one call — the cleanest way to cache an expensive QuerySet result:

Recomputing Every Request vs Caching the Result

Recomputed Every Time
def expensive_stats(): return Book.objects.aggregate( avg_price=Avg("price"), total=Count("id") ) def stats_view(request): stats = expensive_stats() # runs the aggregate query EVERY request
Cached With get_or_set()
def stats_view(request): stats = cache.get_or_set("book_stats", expensive_stats, timeout=600) # runs the query ONCE per 10 minutes; every other request within # that window gets the cached result instantly

Per-View Caching: @cache_page

The coarsest-grained option — caches an entire view's rendered response:

from django.views.decorators.cache import cache_page @cache_page(60 * 15) # cache this view's response for 15 minutes def book_list(request): books = Book.objects.all() return render(request, "catalog/book_list.html", {"books": books})

Template Fragment Caching

@cache_page caches the whole response — {% cache %} caches just one part of a template, leaving the rest (a per-user greeting, say) to render fresh every time:

{% load cache %} <h1>Welcome, {{ request.user.username }}!</h1> <!-- always fresh, never cached --> {% cache 900 book_list_fragment %} <ul> {% for book in books %} <li>{{ book.title }}</li> {% endfor %} </ul> {% endcache %}

900 is the timeout in seconds; book_list_fragment is an arbitrary name identifying this cached fragment.

Varying the Cache Key

A page cached for one visitor must not be served to another if the content actually differs per-user — Django's vary_on_* decorators fold request details into the cache key itself:

from django.views.decorators.vary import vary_on_cookie from django.views.decorators.cache import cache_page @vary_on_cookie @cache_page(60 * 15) def dashboard(request): # now cached SEPARATELY per session cookie — one user's # cached dashboard is never served to a different user ...

💻 Coding Challenges

Challenge 1: Cache an Expensive Computation

Write a function get_catalog_stats() that aggregates the average price and total count of all books, then use cache.get_or_set() to cache the result for 10 minutes inside a view.

Goal: Practice the low-level cache API's get_or_set() pattern for an expensive query.

→ Solution

Challenge 2: Cache a Template Fragment

Wrap the book list portion of a template in {% cache %} for 5 minutes, while leaving a per-user "Welcome, {{ request.user.username }}" line outside the cached block so it still renders fresh for every request.

Goal: Practice caching only part of a page, not the whole response.

→ Solution

Challenge 3: Invalidate the Cache on Save

Write a post_save signal receiver (per Chapter 4's pattern) on the Book model that calls cache.delete("book_stats") whenever a book is created or updated, so get_catalog_stats()'s cached value from Challenge 1 never goes stale for longer than necessary.

Goal: Practice tying cache invalidation to the actual event that makes cached data stale, instead of just waiting out a timeout.

→ Solution

⚠️ Gotcha: Cache Invalidation Is the Hard Part

Caching a book list for 15 minutes means a newly added book — or a price correction — simply won't appear for up to 15 minutes, unless something explicitly clears that cache entry. A timeout alone is a blunt instrument; tying invalidation to the actual event that makes the cache stale (a post_save signal calling cache.delete(), the same signal pattern from Chapter 4) is what keeps a cache both fast and correct. Also worth knowing: if local development uses DummyCache (or never configures a real backend), caching bugs — including a forgotten invalidation — will never surface until the app is running against a real cache backend in production.

🎯 What's Next

With expensive data now cached correctly, the next chapter returns to class-based views from Course 1's introduction and goes much further: Class-Based Views Deep Dive — Django's generic views and mixins for building common patterns with even less code.