Django caching: per-view, fragment and low-level caching

Pick a Django cache backend, use cache_page, template fragment caching and the low-level API safely, and invalidate with signals and versioned keys.

8 min read
On this page 9 sections
  1. Where application caching fits
  2. Choosing a cache backend
  3. Per-view caching with cache_page
  4. Template fragment caching
  5. Low-level caching and cached_property
  6. Invalidation with signals and versioned keys
  7. Pitfalls on authenticated pages
  8. Key takeaways
  9. Frequently asked questions

Django's cache framework gives you four levels of caching: the whole site through cache middleware, a single view with cache_page, part of a template with the {% cache %} tag, and individual values through the low-level API (cache.get, cache.set and friends). For most apps, the low-level API backed by Redis does the heavy lifting, while view and fragment caching suit public pages. The main risk is caching something personal under a key that other users can hit.

Where application caching fits

By the time a request reaches Django, the browser, the CDN and perhaps Nginx have already had their chance to answer it. Application caching is for what they can't store: pages that are partly personal, values used by many different pages, and results of expensive queries that feed several views. It is the application layer of the stack in our guide to types of caching.

The rule that runs through everything below: a cache key must include everything that changes the output. If a course page looks different for enrolled students, the key needs to know that. If it doesn't, someone will see the wrong page.

Choosing a cache backend

BackendShared across servers?When to use it
Redis (django.core.cache.backends.redis.RedisCache, built in since Django 4.0)YesThe default choice for production; also handles sessions, counters and locks
Memcached (PyMemcacheCache or PyLibMCCache)YesA simple, fast shared cache when you need nothing else
Database cacheYesSmall sites with no cache server; it adds load to the database you are trying to protect
File-based cacheOnly on one serverSingle-server setups
Local memory (LocMemCache)No, one copy per processDevelopment, or tiny values that can be stale; it is the default if you configure nothing
Dummy cacheCaches nothingTests and development

The local-memory default catches teams out. Each Gunicorn worker has its own copy, so a cache.delete() in one worker leaves stale values in all the others. In production, use Redis or Memcached:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://10.0.0.5:6379/1",
        "TIMEOUT": 300,          # default lifetime in seconds
        "KEY_PREFIX": "lms",
    }
}

Install redis-py (hiredis is recommended too). TIMEOUT defaults to 300 seconds; None means keys never expire and 0 means don't cache. LOCATION can also be a list, in which case Django writes to the first server and reads from the others.

Per-view caching with cache_page

cache_page stores a view's whole response, keyed by its URL:

from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie

@cache_page(60 * 5)
def course_catalogue(request): ...

@cache_page(60)
@vary_on_cookie             # inner: sets Vary before the response is cached
def my_courses(request): ...

Four things to know, most of them spelled out in the Django documentation:

  • The key is the URL. Query strings count, so ?page=2 is cached separately. With USE_I18N and USE_TZ, the language and time zone are added. Nothing else is, unless the response carries a Vary header.

  • It doesn't vary by user. cache_page is applied to the view directly, so it caches the response before response middleware runs, including the session middleware that would add Vary: Cookie. Put @vary_on_cookie inside cache_page, as above, or keep cache_page off personal views altogether.

  • Forms need care. For views that render a CSRF-protected form, apply csrf_protect inside cache_page so the CSRF cookie and Vary header are set before caching, as the CSRF guide explains.

  • It also sets browser headers. cache_page adds Cache-Control: max-age and Expires to the response, so browsers and any CDN in front may keep the page too. If you want a shorter downstream lifetime, add @cache_control(max_age=30); Django keeps the smaller of the two max-age values in the header.

The per-site cache (UpdateCacheMiddleware first in MIDDLEWARE, FetchFromCacheMiddleware last) applies the same idea to every page. It is safer than it looks, because it skips responses marked private, no-cache or no-store and responses that set a cookie while varying on Cookie. Still, most learning platforms have too many personal pages for a site-wide cache to pay off.

Template fragment caching

When most of a page is personal but one block is expensive and shared, cache the block:

{% load cache %}
{% cache 3600 course_syllabus course.id course.updated_at|date:"U" %}
    ... syllabus with modules, lesson counts and durations ...
{% endcache %}

Every extra argument after the fragment name becomes part of the key. Including course.updated_at means that when a teacher edits the course, the next render uses a new key and the old fragment simply ages out, with no explicit invalidation (as long as editing a lesson also updates the course's timestamp). If you do need to delete a fragment, make_template_fragment_key("course_syllabus", [course.id, stamp]) rebuilds its key. Two cautions: arguments are converted to strings, so use IDs rather than model instances, and never cache a block that contains something personal without the user's ID in the key.

Low-level caching and cached_property

The low-level API is where most useful caching happens, because you choose exactly what to store:

from django.core.cache import cache

def course_outline(course_id):
    key = f"course:{course_id}:v{course_version(course_id)}:outline"
    return cache.get_or_set(key, lambda: build_outline(course_id), timeout=3600)

get_or_set returns the cached value or calls the function, stores its result and returns it. Other methods worth knowing: get_many and set_many batch several keys into one round trip, add stores a value only if the key is absent, and touch extends a key's lifetime. Every method has an async twin prefixed with a, such as aget and aset. Two gotchas: cache.get returns None for a missing key, so use a sentinel default if None is a legitimate value, and incr isn't guaranteed to be atomic on backends that don't support it natively.

cached_property is a different thing. It caches a method's result on one object for as long as that object exists, which in a web app usually means one request. It saves calling the same expensive method twice while rendering a page; it doesn't help the next request.

Invalidation with signals and versioned keys

The course_outline example uses a version number in its key. Here is the other half:

import time
from django.db import transaction
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver

def course_version(course_id):
    return cache.get_or_set(f"course:{course_id}:ver", time.time_ns, timeout=None)

@receiver([post_save, post_delete], sender=Lesson)
def lesson_changed(sender, instance, **kwargs):
    key = f"course:{instance.course_id}:ver"
    transaction.on_commit(lambda: cache.set(key, time.time_ns(), timeout=None))

Changing one version key makes every cached value for that course unreachable at once, whether it's the outline, lesson counts or the syllabus fragment, without hunting down each key. The old entries expire on their own. Three details matter:

  • transaction.on_commit. Bump the version only after the transaction commits. If you do it inside the transaction, another request can rebuild the cache from the old, still-committed data in between. Django's transaction docs list cache invalidation as a typical use of on_commit.

  • A timestamp, not a counter. If the version key is ever evicted, a counter would restart at 1 and could resurrect entries cached long ago under v1. A fresh timestamp can't collide with an old one.

  • Signals don't fire for everything. QuerySet.update() and bulk_create() skip post_save, so code that uses them must bump the version itself.

For how this compares with TTL-only and event-based approaches, see cache invalidation. For the patterns behind get_or_set, see caching strategies, and for sizing and eviction on the Redis side, Redis caching.

Pitfalls on authenticated pages

  1. cache_page on a personal view without vary_on_cookie serves the first student's page to everyone who opens that URL.

  2. Fragment keys without the user. A "Continue watching" block cached as {% cache 600 continue_watching %} is shared by the whole site.

  3. Caching permissions. Cache what a course contains, not whether this student may see it. Check enrolment on every request, or a refunded student keeps access until the entry expires.

  4. No stampede protection. get_or_set doesn't stop a hundred workers from rebuilding the same expired key at once, which matters for hot keys during a live class.

Key takeaways

  • Use Redis or Memcached in production; the local-memory default gives every worker process its own cache.

  • cache_page keys on the URL only and caches before response middleware runs, so keep it off personal views or add vary_on_cookie inside it.

  • Fragment caching with updated_at in the key gives you invalidation for free.

  • The low-level API with get_or_set covers most needs; cached_property only lasts as long as one object.

  • Invalidate by bumping a timestamp version inside transaction.on_commit, and remember that update() skips signals.

Frequently asked questions

How to use cache in Django?

Configure a backend in the CACHES setting, then pick a level. For public pages that are the same for everyone, wrap the view in cache_page. For a shared block inside a personal page, use the {% cache %} template tag. For anything else, call the low-level API: cache.get_or_set(key, function, timeout) returns a cached value or computes and stores it. Make sure every key includes whatever changes the output.

How to use Redis cache in Django?

Run a Redis server, install the redis package (and optionally hiredis), and set BACKEND to django.core.cache.backends.redis.RedisCache with LOCATION pointing at your server, such as redis://10.0.0.5:6379/1. Django's cache API then stores values in Redis, shared by all your app servers. Set a KEY_PREFIX if other apps use the same Redis, and choose an eviction policy suited to caching on the Redis side.

How to cache in Django?

Start by finding what is expensive and repeated: a slow query, a heavy template block, a page many students open at once. Cache the smallest thing that removes the cost, give it a lifetime that matches how stale it may be, and decide how it will be invalidated before you ship it. Then measure hit rates and response times, because a cache that rarely hits only adds another network call.

Share this article

Looking for something else?

Talk to Us