Skip to content
C Codeloom
Django

Django Caching Strategies

Compare per-view, template fragment, low-level, and per-site caching in Django and learn when each pays off.

·4 min read · By Codeloom
Intermediate 10 min read

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.get and cache.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
Request path with caching

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 Vary headers. If your page changes based on Accept-Language or Cookie, the cache key needs to reflect that. Use vary_on_headers or vary_on_cookie.
  • Cache stampedes. When a hot key expires, hundreds of requests can rebuild it at once. Use cache.add with 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-toolbar or django-silk and 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.