Django Caching Strategies
Compare per-view, template fragment, low-level, and per-site caching in Django and learn when each pays off.
What you'll learn
- ✓Cache backends
- ✓Per-view vs low-level
- ✓Cache invalidation
- ✓Vary headers
- ✓When not to cache
Prerequisites
- •Familiar with Django views and templates
What and Why
A cache trades memory for time. Instead of recomputing a page or rerunning a query, you store the result and serve it again the next time the same input arrives. In Django, caching is built into the framework at multiple layers, and choosing the right layer is more important than choosing the fastest backend.
The goal is to reduce latency for users and load on your database. The cost is correctness risk: if the cache returns stale data, your users see it. Every caching decision is a tradeoff between speed and freshness.
Mental Model
Caching in Django lives at four levels, from coarse to fine.
- Per-site: every page is cached for anonymous users by middleware. Useful for marketing sites.
- Per-view: one decorator caches a single view’s response.
- Template fragment: cache a chunk of HTML inside a template.
- Low-level: cache arbitrary objects, like a query result or computed value, with
cache.getandcache.set.
You pick the level by asking “what is the smallest thing I can cache that still moves the needle?” If a whole page is identical for all users, cache the page. If only one slow widget is the problem, cache the widget.
Hands-on Example
Configure Redis as your cache backend in settings.py.
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://localhost:6379/2",
"TIMEOUT": 300,
}
}
Per-view caching for a public landing page.
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def homepage(request):
return render(request, "home.html", context())
Template fragment caching for a slow sidebar.
{% load cache %}
{% cache 600 sidebar request.user.id %}
{% include "_sidebar.html" %}
{% endcache %}
Low-level caching for an expensive aggregate.
from django.core.cache import cache
def top_authors():
key = "top_authors_v1"
data = cache.get(key)
if data is None:
data = list(
Author.objects.annotate(n=Count("articles"))
.order_by("-n")[:10]
.values("id", "name", "n")
)
cache.set(key, data, timeout=300)
return data
Request -> Middleware -> Cache check
|
hit | miss
v | v
Response | View -> ORM -> DB
| |
| v
+---- Cache set -> Response Invalidation matters as much as setting. When an article is created, bust the related caches.
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Article)
def invalidate_author_cache(sender, instance, **kwargs):
cache.delete("top_authors_v1")
The _v1 suffix on the key is a small but powerful pattern. When you change the shape of cached data, bump the version. Old entries simply expire while new code reads new keys, no manual flushing required.
Common Pitfalls
- Caching authenticated pages. The default per-site cache only handles anonymous traffic. Caching pages for logged-in users without keying on user id leaks data across sessions.
- Ignoring
Varyheaders. If your page changes based onAccept-LanguageorCookie, the cache key needs to reflect that. Usevary_on_headersorvary_on_cookie. - Cache stampedes. When a hot key expires, hundreds of requests can rebuild it at once. Use
cache.addwith a short-lived lock, or stale-while-revalidate patterns. - Caching slow queries instead of fixing them. A missing index masked by a cache will bite you the moment the cache fails over.
- Treating the cache as a database. Caches are allowed to disappear. Never store the only copy of important data there.
Practical Tips
- Measure first. Wrap suspect views in
django-debug-toolbarordjango-silkand confirm where time actually goes. - Use short timeouts plus signal-based invalidation rather than long timeouts and hope.
- Namespace keys with the model name and version, like
articles:list:v2:author=17. - Different backends for different needs. Redis for shared state, local memory for hot per-process lookups.
- Cache the heavy bits, not the whole response. A 50 ms view that calls a 400 ms aggregate is fixed by caching the aggregate alone.
Wrap-up
Caching in Django is a layered toolbox, not a single switch. Start with the smallest unit that hurts, pick the matching level, and pair every cache.set with a clear invalidation plan. Version your keys, key by user when content varies, and remember that a cache is a performance optimization, not a source of truth. Done well, caching can take a heavy database off its knees with surprisingly little code.
Related articles
- Django Django Celery Task Queue Tutorial
A practical guide to wiring Celery into Django for background work, scheduled jobs, and reliable task processing.
- Django Django Admin Customization: A Practical Tutorial
Go beyond the default Django admin: customize ModelAdmin classes, list views, search, filters, inline editing, and admin actions to build a usable backoffice your team will actually enjoy.
- Django Django Class-Based Views: A Practical Tutorial
Understand Django's class-based views by building from View up to ListView and UpdateView. Learn the MRO, mixins, and when CBVs beat function-based views in real projects.
- Django Django Middleware and the Request Lifecycle
Understand how a Django request flows through middleware, URL routing, and views, and learn to write custom middleware for cross-cutting concerns.