Django's caching framework
Django's cache API is the same cache-aside pattern from the system design lessons, with a specific set of layers — per-view, template fragment, and low-level — each trading precision for convenience differently.
3 min read
The low-level API — cache-aside, explicitly
from django.core.cache import cache
def get_dashboard_stats(user_id):
key = f"dashboard-stats:{user_id}"
stats = cache.get(key)
if stats is None:
stats = compute_expensive_stats(user_id) # the real work
cache.set(key, stats, timeout=300) # 5 minutes
return statsThis is the cache-aside pattern directly: check the cache first, compute and store on a miss, return the cached value on a hit. Django's cache object talks to whatever backend CACHES in settings.py points at — Redis and Memcached are the common production choices; LocMemCache (per-process memory) is the default and fine for development, but doesn't share state across multiple server processes or machines, which matters the moment an app runs behind more than one worker.
Per-view caching — the coarsest, least precise layer
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # cache the entire rendered response for 15 minutes
def article_list(request):
articles = Article.objects.all()
return render(request, "articles/list.html", {"articles": articles})cache_page caches the entire HTTP response, keyed by the URL (and, by default, query string) — the next matching request within the timeout skips the view function, the database queries, and the template rendering entirely, returning the cached response directly. This is powerful specifically because it's coarse: it's also why it's wrong for any page whose content differs per user (a logged-in user's dashboard) unless configured carefully with Vary headers, since a naive setup would serve one user's cached page to another.
Template fragment caching — caching part of a page
{% load cache %}
{% cache 500 sidebar request.user.id %}
{# expensive sidebar rendering here #}
{% endcache %}Between "cache the whole response" and "cache raw data" sits fragment caching — caching just the rendered HTML of one expensive template section, while the rest of the page still renders fresh on every request. The extra arguments after the timeout (request.user.id here) become part of the cache key, so different users get different cached fragments instead of one user's sidebar leaking into another's page — this is the templating-layer version of the same "what's actually in the cache key" question that matters for the low-level API too.
The part every caching lesson eventually has to say: invalidation
def update_profile(request):
request.user.bio = request.POST["bio"]
request.user.save()
cache.delete(f"dashboard-stats:{request.user.id}") # don't forget thisThis is the same hard problem the system design caching lesson covers, showing up concretely in Django: every code path that changes data has to remember to invalidate the corresponding cache key, and a forgotten cache.delete() here means get_dashboard_stats keeps returning stale data for up to 5 minutes after a real change — silently, with no error to signal it. Django doesn't solve this automatically; a common, more automatic-feeling approach is a post_save signal that invalidates related cache keys whenever a model instance changes, trading the earlier lesson's signal downsides (easy to miss, silently skipped by bulk operations) for not having to remember the cache.delete() call at every individual call site.
Choosing which layer, concretely
Per-view caching fits pages that render identically for everyone and change rarely (a public marketing page, an article list). Fragment caching fits one genuinely expensive piece of an otherwise-dynamic page (a sidebar with an expensive aggregate query, on a page that's otherwise per-user). The low-level API fits anything more specific than "cache this template output" — caching a computed value, an API response, or data reused across several different views, where the caller controls exactly what the key is and when it gets invalidated.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. In the low-level cache API example, `if stats is None: stats = compute_expensive_stats(...); cache.set(...)`, what pattern is this?
2. Why is @cache_page risky on a page whose content differs per logged-in user, without careful Vary header configuration?
3. In `{% cache 500 sidebar request.user.id %}`, what is request.user.id actually doing there?
4. Why is cache invalidation described as 'the hard part' even in Django's caching framework?