Django's Cache Framework
Redis itself, as a general-purpose tool you talk to directly from Python (data structures, cache-aside, invalidation, thundering-herd stampedes), belongs to its own lesson. This lesson is about the layer Django builds on top of that: its own CACHES framework, meaning the pluggable backend system, the four levels of granularity you can cache at, and how Django wires caching into views, templates, and HTTP headers.
Configuration and backends
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
"TIMEOUT": 300, # seconds; None = never expires
"KEY_PREFIX": "myapp", # namespacing across apps sharing one backend
}
}Django doesn't hardcode Redis. CACHES names a backend class, and swapping it changes where cached data actually lives without touching any of your call sites.
| Backend | Persistent | Distributed | Notes |
|---|---|---|---|
LocMemCache (default) |
No, process memory | No | Zero setup; dev only, never shared across processes |
django_redis.cache.RedisCache |
Yes | Yes | The production default; a plain Redis instance wired into Django's cache API |
PyMemcacheCache |
No | Yes | Fast, distributed, no complex data types |
DatabaseCache |
Yes | Yes | Uses a real table (manage.py createcachetable); slower, but needs no new infrastructure |
FileBasedCache |
Yes | No | Dev or low-traffic only |
DummyCache |
N/A | N/A | Accepts every call and does nothing; swaps caching off in a test or staging environment without touching a single call site |
Remember:
CACHES["BACKEND"]is the one setting that decides where cached data actually lives. Everything else in this lesson works the same regardless of which backend is plugged in.
Four levels of granularity
Per-site -> Per-view -> Template fragment -> Low-level (manual)
(all) (one view) (part of a page) (any Python object)Per-view is the one you'll reach for most:
from django.views.decorators.cache import cache_page, never_cache
@cache_page(60 * 15)
def article_list(request):
...
@never_cache
def user_dashboard(request): # personalized -- never cache
...Template fragment caches part of a page rather than the whole response:
{% load cache %}
{% cache 500 sidebar %}
{% for item in sidebar_items %}<li>{{ item }}</li>{% endfor %}
{% endcache %}
{# vary per-user by passing an extra key #}
{% cache 500 user_sidebar request.user.id %}...{% endcache %}The low-level API caches any Python object manually, and it's the one that maps most directly onto the general cache-aside pattern: check the cache, fall back to computing the value on a miss, then write it back.
from django.core.cache import cache
cache.set("key", value, timeout=300)
value = cache.get("key", default=None)
cache.delete("key")
cache.get_or_set("key", expensive_fn, 300) # cache-aside in one call
cache.add("key", value, timeout=10) # sets only if NOT already present, atomic, used for locksRemember: per-view is the everyday default. Reach for template fragments when only part of a page is expensive, and the low-level API when you're caching something that isn't a whole HTTP response at all.
Invalidation: signals are Django's specific hook
The general cache-aside invalidation rule is delete-on-write: whenever the underlying data changes, delete the cached copy so the next read recomputes it. Django's specific mechanism for that is model signals, so invalidation fires automatically wherever a model is saved, not just from the one write path you remembered to update by hand.
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
@receiver(post_save, sender=Article)
def invalidate_article_cache(sender, instance, **kwargs):
cache.delete(f"article:detail:{instance.pk}")
@receiver(post_delete, sender=Article)
def invalidate_on_delete(sender, instance, **kwargs):
cache.delete(f"article:detail:{instance.pk}")This is stronger than invalidating by hand inside a view or serializer's save path. A signal fires no matter which code path triggered the save, the admin, a shell session, a management command, or a bulk operation calling .save() on each object individually, so there's no forgotten call site quietly leaving a stale cache entry behind.
Remember: a model signal invalidates the cache no matter which code path saved the model. A manual cache.delete() inside one view only protects that one path.
Caching a queryset: the gotcha specific to Django
def get_published_articles():
key = "articles:published"
articles = cache.get(key)
if articles is None:
articles = list( # must evaluate -- a lazy queryset doesn't serialize
Article.objects.filter(status="published").select_related("author")
)
cache.set(key, articles, 600)
return articlesDjango querysets are lazy, and that laziness has a specific consequence for caching: caching an unevaluated queryset either fails to serialize outright, or, worse, silently re-runs the underlying query every time the "cached" value is read back, which defeats the cache entirely while looking like it's working. Always call list() on the queryset before handing it to cache.set, exactly as above, so what's actually stored is the materialized rows, not a lazy description of how to fetch them.
Remember: call
list()on a queryset before caching it. Caching the lazy queryset itself, not its evaluated rows, silently re-runs the query on every read.
HTTP cache headers
from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_cookie
@cache_control(max_age=3600, public=True) # browsers/CDN may cache
def public_page(request): ...
@cache_control(private=True, max_age=0) # never cache in a shared proxy/CDN
def user_profile(request): ...
@vary_on_cookie # separate cached copy per session
@cache_page(600)
def my_view(request): ...These decorators control caching in the browser and in any CDN sitting in front of Django, a separate layer from everything above. @cache_control and @vary_on_cookie never touch Django's own server-side cache backend; they only set response headers that other HTTP caches read and obey.
Remember:
@cache_control/@vary_on_cookieare instructions for caches outside Django (browser, CDN). They don't store anything in Django's ownCACHESbackend.