Caching
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06), Python 3.12-3.14 and
PostgreSQL 15 or newer, checked on 2026-10-03; where Django 5.2 LTS (5.2.17) behaves differently the text says so. Every listing was run in a
scratch project on Python 3.13.16 and Django 6.1.1 with SQLite: manage.py check, a suite of 28 tests (manage.py test) and a curl session
against runserver all passed, and the output shown is the real output. There was no Redis, Memcached or PostgreSQL in the scratch environment.
The Redis backend was exercised against fakeredis, a Python server that speaks the Redis protocol over TCP, so Django’s real RedisCache
code ran, but it is not a performance or persistence test. The Memcached backend was only configured and instantiated, never connected to a
server; the text marks the listings that depend on either.
Caching means saving the result of expensive work so that the next request can skip it. Django offers it at four levels of granularity: the
whole site, a single view, a fragment of a template, and arbitrary Python values through a low-level API. Underneath all four sits one
configuration object, the CACHES setting, which decides where the data lives. A second, separate topic is downstream caching: the browser,
a CDN or a proxy that sit between the visitor and Django and that you steer with HTTP headers (Vary, Cache-Control). This page covers both,
and ends with the part that is hardest in practice: knowing when a cached value is no longer true. The settings file that holds CACHES is
covered on Settings & Configuration, the middleware machinery on
Middleware, the template tag on Templates, and the cache-backed session store on
"Sessions, Cookies, Messages & Signing".
In a hurry: what to use for what
| I need to… | Use | Section |
|---|---|---|
Pick a production cache |
|
|
Cache while developing, or in tests |
|
|
Cache everything for anonymous traffic |
|
|
Cache one expensive view |
|
|
Cache one expensive piece of a page |
The |
|
Cache a query result or an API response |
|
|
Use the cache from an |
|
|
Keep one URL from serving the wrong variant |
|
|
Stop browsers and proxies caching private data |
|
|
Remove stale data after a change |
|
The Bookshelf pieces used on this page
The running example is the Bookshelf catalogue of books that grows across this section, in an app named catalog. This page needs only a
small model, so that the cached work (a count, a title lookup) is real database work.
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
price = models.DecimalField(max_digits=6, decimal_places=2, default=0)
def __str__(self):
return self.title
Source: Django: Models
Configuring caches: CACHES
CACHES is a dictionary of named caches. The alias default is mandatory: manage.py check fails with caches.E001 when it is missing.
Each entry has a BACKEND (a dotted path to the cache class) and the common arguments in the table below. Other aliases are ordinary names
that code or settings refer to, such as CACHE_MIDDLEWARE_ALIAS, cache_page(cache="…") or the using argument of the template tag.
| Key | Meaning |
|---|---|
|
Dotted path of the cache class. A path to your own subclass is allowed, but the built-in backends are the well-tested choice. |
|
Backend specific: a URL (Redis), |
|
Default lifetime in seconds. The default is 300. |
|
Backend specific. For the backends that cull on their own (local memory, file, database): |
|
String prepended to every key; empty by default. Use it when several Django instances or environments share one cache server. |
|
Default version number combined into every key; 1 by default. See Keys, prefixes and versions. |
|
Dotted path of a function that composes prefix, version and key into the final key. |
The scratch project defines one alias per backend. The local-memory default is the one the tests use; the others are shown in the sections
that follow.
import os
import tempfile
from pathlib import Path
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "bookshelf-default",
"KEY_PREFIX": "bookshelf",
"TIMEOUT": 300,
"OPTIONS": {"MAX_ENTRIES": 1000, "CULL_FREQUENCY": 3},
},
"template_fragments": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "bookshelf-fragments",
},
"dbcache": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "bookshelf_cache_table",
},
"files": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": os.environ.get("CACHE_DIR", str(Path(tempfile.gettempdir()) / "bookshelf_cache")),
},
"redis": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": os.environ.get("REDIS_URL", "redis://127.0.0.1:6379"),
},
"memcached": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "127.0.0.1:11211",
},
"dummy": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"},
}
CACHE_MIDDLEWARE_ALIAS = "default"
CACHE_MIDDLEWARE_SECONDS = 600
CACHE_MIDDLEWARE_KEY_PREFIX = "bookshelf"
Source: Django: Settings - CACHES
The name template_fragments is not arbitrary: it is the alias the {% cache %} tag looks for first (see Template fragment caching). The project passes
manage.py check and manage.py check --tag caches with this settings file.
The backends
| Backend | Class | Shared between processes? | Good for |
|---|---|---|---|
Redis |
|
Yes |
Production. Needs the |
Memcached |
|
Yes |
Production. Memory only, so a restart empties it. |
Database |
|
Yes |
When no cache server is available and the database is fast and well indexed. |
File system |
|
Yes, on one host |
Small deployments and development. |
Local memory |
|
No: one private cache per process |
Development and tests. It is the default when |
Dummy |
|
Not applicable |
Keeping the caching code while disabling it in development or tests. |
No built-in backend is a database of record. Every one of them is allowed to forget a key at any moment (expiry, culling, eviction, a restart), so the code must always be able to rebuild the value.
Redis
Redis is an in-memory data store that can also act as a cache. Django has shipped a Redis cache backend since 4.0, using redis-py as the
client. Install redis (and, recommended, hiredis), run a server, and point LOCATION at its URL. Credentials belong in the URL, which
should come from the environment and never from the settings file:
import os
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": os.environ["REDIS_URL"], # e.g. redis://<user>:<password>@<host>:6379
"OPTIONS": {"db": "10", "pool_class": "redis.BlockingConnectionPool"},
}
}
Source: Django: Redis cache backend
OPTIONS is passed to redis-py: the example selects logical database 10 and a blocking connection pool (redis.ConnectionPool is the default).
LOCATION may also be a list. With several servers in replication mode, writes go to the first (the leader) and reads go to one of the others
chosen at random:
"LOCATION": [
"redis://127.0.0.1:6379", # leader
"redis://127.0.0.1:6378", # read replica 1
"redis://127.0.0.1:6377", # read replica 2
],
Source: Django: Redis cache backend
Replicas lag, so a read straight after a write can return the old value: do not use a replicated cache for values that must be read back
immediately. The scratch run pointed the redis alias at a fakeredis TCP server on a local port and ran the real backend against it:
>>> from django.core.cache import caches
>>> r = caches["redis"]
>>> type(r).__name__
'RedisCache'
>>> r.set("k", {"a": 1}, 30)
>>> r.get("k"), r.add("k", 2)
({'a': 1}, False)
>>> r.set("n", 1); r.incr("n")
2
>>> r.touch("k", 10), r.delete("k")
(True, True)
>>> r.make_key("k")
':1:k'
Values are stored pickled and Redis holds them under the key :1:k: an empty prefix, the version and the name, joined with colons. The
server’s maxmemory and eviction settings decide what happens when memory runs out; for a pure cache, an allkeys-* policy is the usual
choice. Those are Redis concerns and are covered in the Redis section: "Architecture, Keyspace & Eviction" (expiration and the maxmemory policies), Use Cases & Patterns (cache-aside
and its variants), Clients & Client-Side Caching, Redis Security (the cache server must not be reachable from outside) and Getting Started. The Spring flavour
of the same idea is on Spring Boot: Redis as a Cache.
Memcached
Memcached is a memory-only cache daemon with a deliberately small feature set: add, get and delete. After installing it you need a Python
binding; Django supports two, pymemcache (PyMemcacheCache) and pylibmc (PyLibMCCache). LOCATION is ip:port, a unix: socket path,
or a list (or a comma or semicolon separated string) of servers; the client spreads keys over all of them, so the group behaves as one big
cache with no duplication.
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": [
"172.19.26.240:11211",
"172.19.26.242:11211",
],
"OPTIONS": {
"no_delay": True,
"ignore_exc": True,
"max_pool_size": 4,
"use_pooling": True,
},
}
}
Source: Django: Memcached
In this example ignore_exc makes network errors behave as cache misses, use_pooling keeps clients connected, and no_delay sets
TCP_NODELAY. PyMemcacheCache also sets allow_unicode_keys, default_noreply and a pickle serde for you; override them in OPTIONS.
In the scratch project the memcached alias was instantiated (caches["memcached"] returned a PyMemcacheCache with its server list) but, with
no Memcached daemon available, no value was stored or read.
Database
The database backend keeps entries in a table. Use it only when you have a fast, well-indexed database and no cache server. LOCATION is the
table name, which must not clash with an existing table. Create the table before first use with the createcachetable command; like migrate
it never touches a table that already exists, and --dry-run prints the SQL instead of running it:
python manage.py createcachetable --dry-run
python manage.py createcachetable
On SQLite the dry run printed:
CREATE TABLE "bookshelf_cache_table" (
"cache_key" varchar(255) NOT NULL PRIMARY KEY,
"value" text NOT NULL,
"expires" datetime NOT NULL
);
CREATE INDEX "bookshelf_cache_table_expires" ON "bookshelf_cache_table" ("expires");
Source: Django: Database caching
The database cache cannot cull expired rows by itself. An expired entry is deleted when a get() finds it, and expired entries are culled when
add(), set() or touch() push the table past MAX_ENTRIES. With several databases, the table behaves as a model called CacheEntry in an
application called django_cache, so a router decides where it lives: send reads to a replica and writes (and allow_migrate) to the primary,
as the documentation’s CacheRouter example does for cache_replica and cache_primary. Without routing instructions the default database is
used. The database side of that is on "Transactions, Raw SQL & Multiple Databases"
and PostgreSQL & Database Backends.
Django 6.1 made the database backend validate its stored values strictly: it now decodes them with strict Base64 validation and raises an exception on invalid data instead of ignoring it. Values written by Django are always valid, but old rows that were corrupted or written by other code may no longer be readable after the upgrade.
File system
The file-based backend writes each value to its own file. LOCATION must be an absolute directory (with a drive letter on Windows) that
exists and is readable and writable by the web server user, or that the user can create:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/django_cache",
"TIMEOUT": 60,
"OPTIONS": {"MAX_ENTRIES": 1000},
}
}
Source: Django: Filesystem caching
Two warnings from the documentation matter. The files are pickles: anyone who can write to the directory can make your site execute code, so the
directory must never be inside MEDIA_ROOT, STATIC_ROOT or a STATICFILES_DIRS path. Django checks for this in check --deploy; with the
cache moved into STATIC_ROOT the scratch project reported:
?: (caches.W002) Your 'files' cache configuration might expose your cache or lead to corruption of your data because its LOCATION is inside STATIC_ROOT.
The second warning: a directory with a very large number of files gets slow. Use another backend or subclass FileBasedCache with a better
culling strategy.
Local memory
The local-memory backend is the default and is thread safe, with least-recently-used culling. It is per process: each worker, and each test
process, has a private copy, so nothing is shared and a value cached by one Gunicorn worker is invisible to the next. That is fine for
development and tests; for production it also wastes memory. If you define more than one locmem cache, give each a distinct LOCATION, which
is the name of its memory store.
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
}
}
Source: Django: Local-memory caching
Dummy, and custom backends
DummyCache implements the whole interface and stores nothing. Switch to it in a development or test settings module to keep all the caching
code in place while guaranteeing every request does the real work:
CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
Source: Django: Dummy caching
To use a backend that does not ship with Django, put its dotted import path in BACKEND. Write your own only with a compelling reason; the
built-in ones are the reference implementations to copy from. Third-party packages, such as django-redis (the pre-4.0 way to use Redis), add
features the built-in Redis backend lacks.
The per-site cache
The simplest use of the framework caches the entire site. Add two middleware classes, the "update" half first and the "fetch" half last:
MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
]
CACHE_MIDDLEWARE_ALIAS = "default" # which cache holds the pages
CACHE_MIDDLEWARE_SECONDS = 600 # default lifetime of a cached page
CACHE_MIDDLEWARE_KEY_PREFIX = "bookshelf" # unique per site when the cache is shared
Source: Django: The per-site cache
The three settings are CACHE_MIDDLEWARE_ALIAS (default 'default'), CACHE_MIDDLEWARE_SECONDS (default 600) and
CACHE_MIDDLEWARE_KEY_PREFIX (default empty). The middleware caches GET and HEAD responses with status 200 whose request and response
headers allow it; a HEAD request can be answered from a cached GET response because the middleware assumes both produce the same headers.
Each distinct query string is a separate page. On the way out, UpdateCacheMiddleware adds Expires and a Cache-Control: max-age header so
downstream caches follow the same lifetime. A view that sets its own max-age (for example through cache_control) overrides the site-wide
number. When USE_I18N is true the key includes the active language, and when USE_TZ is true it includes the current time zone.
The scratch test ran a GET and then a POST through the pair:
@override_settings(MIDDLEWARE=MW) # the four classes listed above
class PerSiteTests(TestCase):
def test_site_cache(self):
self.client.get("/manual/")
r = self.client.get("/manual/")
self.assertIn("max-age=600", r.headers["Cache-Control"])
r = self.client.post("/manual/")
self.assertNotIn("max-age=600", r.headers.get("Cache-Control", ""))
first in the list] U1 --> M[Other middleware] M --> F[FetchFromCacheMiddleware
last in the list] F -->|hit| R[Cached response] F -->|miss| V[View] V -->|response, stored by Update| C[(Cache)]
The order is not a typo. FetchFromCacheMiddleware runs in the request phase, which goes top to bottom, and needs every header-varying
middleware to have run so it can build the right key; so it goes after them. UpdateCacheMiddleware runs in the response phase, which goes
bottom to top, and must see the final Vary header; so it goes before every middleware that adds to Vary (SessionMiddleware adds
Cookie, GZipMiddleware adds Accept-Encoding, LocaleMiddleware adds Accept-Language). The general ordering rules are on
Middleware.
The site-wide cache is only safe for content that is the same for everybody. It has no notion of "logged in" beyond the Vary: Cookie header
that sessions add, so a site where most pages are personal should cache per view or per fragment instead.
The per-view cache
cache_page from django.views.decorators.cache caches one view’s response for a number of seconds. Its signature is
cache_page(timeout, *, cache=None, key_prefix=None):
from django.http import HttpResponse
from django.views.decorators.cache import cache_page
from .services import book_count
@cache_page(60 * 15, key_prefix="books")
def book_list(request):
return HttpResponse(f"{book_count()} books")
Source: Django: The per-view cache
Like the per-site cache it is keyed off the full URL, so /foo/1/ and /foo/23/ are different entries, and the timeout wins over a
max-age in the view’s own Cache-Control header (a behaviour that changed in Django 3.1). cache="special_cache" selects another alias from
CACHES; key_prefix works like CACHE_MIDDLEWARE_KEY_PREFIX and is concatenated with the cache’s own KEY_PREFIX. cache_page also sets
Cache-Control and Expires for downstream caches. Against runserver two requests gave one render and the headers below:
for i in 1 2; do curl -si localhost:45801/books/ | grep -i -E "^(HTTP|cache-control|expires|vary)|render"; echo --; done
HTTP/1.1 200 OK
Expires: Sat, 03 Oct 2026 01:05:09 GMT
Cache-Control: max-age=900
Vary: Accept-Language
0 books, render #1
--
HTTP/1.1 200 OK
Expires: Sat, 03 Oct 2026 01:05:09 GMT
Cache-Control: max-age=900
Vary: Accept-Language
0 books, render #1
--
The identical render #1 and identical Expires show that the second response came from the cache. (Vary: Accept-Language is there because the
scratch project enables LocaleMiddleware.)
Caching in the URLconf instead
Decorating the view couples it to the cache. To keep the view reusable on a site with no cache, wrap it where it is routed instead:
from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
urlpatterns = [
path("books/", cache_page(60 * 15)(views.book_list)),
]
What cache_page does not do
cache_page is meant for content that is safe to reuse for every request to the same URL. It does not vary by authentication, session or
cookie. Because it wraps the view directly, the response is stored before response middleware runs, so anything a later middleware would add
(a Vary header, a CSRF cookie) is missing from the cached copy. Two scratch views show the first problem. Both read a cookie called who;
the first is cached without Vary, the second varies on Cookie:
@cache_page(60)
def whoami_unsafe(request):
return HttpResponse(f"hello {request.COOKIES.get('who', 'anonymous')}")
@cache_page(60)
@vary_on_cookie
def whoami_safe(request):
return HttpResponse(f"hello {request.COOKIES.get('who', 'anonymous')}")
def test_cache_page_does_not_vary_by_user(self):
Client().get("/whoami-unsafe/") # an anonymous visitor fills the cache
alice = Client(); alice.cookies["who"] = "alice"
self.assertEqual(alice.get("/whoami-unsafe/").content, b"hello anonymous") # the leak
Client().get("/whoami-safe/")
self.assertEqual(alice.get("/whoami-safe/").content, b"hello alice")
The test passes, and that is the problem: the first visitor’s page was served to Alice. Replace the cookie with a user name and the same bug
shows one user another user’s data. The remedy is Vary (Downstream caches and the Vary header) or not caching the page at all (Controlling downstream caches: Cache-Control).
The second problem concerns forms. A view that renders a CSRF token needs the CSRF cookie and Vary: Cookie on the response; when the cache
decorator is applied on the view itself, the CSRF middleware has not yet set them. Put csrf_protect inside cache_page, as the CSRF how-to
documents, and the response is stored with both:
@cache_page(60 * 15)
@csrf_protect
def contact(request):
return render(request, "catalog/contact.html")
The test for it asserted Cookie in Vary and a csrftoken cookie in the response; the curl run printed Vary: Cookie, Accept-Language
and a Set-Cookie: csrftoken=… line. With the per-site middleware pair, UpdateCacheMiddleware sits first and the problem does not arise. More on
CSRF is on Security.
Template fragment caching
For finer control, cache only the expensive piece of a template with the cache tag from the cache library. It takes at least two arguments,
the timeout in seconds and a fragment name; the name is taken literally, never as a variable. A timeout of None caches forever, and the
timeout may be a template variable that resolves to an integer.
{% load cache %}
<h1>Bookshelf</h1>
{% cache 300 sidebar %}<aside>{{ stats }}</aside>{% endcache %}
{% cache 300 profile request.user.username using="default" %}<p>profile {{ stats }}</p>{% endcache %}
Extra arguments after the name identify one cached copy each; here every user name gets its own profile fragment. They are converted to
strings to build the key, so choose values that are unique as strings: None and "None" collide, and a model instance is risky because its
__str__() need not be unique. request.user.username is unique for the default User model. If USE_I18N is on, the per-site cache respects
the active language, but the tag does not: add a language variable (LANGUAGE_CODE from {% get_current_language as LANGUAGE_CODE %}) as a
vary argument.
By default the tag stores fragments in the cache called template_fragments and falls back to default when no such alias exists. The
using keyword, which must be the last argument, selects another cache; naming a cache that is not configured is an error. This explains a
detail of the scratch project: the sidebar fragment, which has no using, lands in template_fragments because that alias exists, while the
profile fragment lands in default. The test checks both stores:
def test_fragment(self):
self.client.get("/home/"); self.client.get("/home/")
self.assertEqual(views.RENDERS["sidebar"], 2) # sidebar + profile, once each
key = make_template_fragment_key("profile", [""])
self.assertIsNotNone(cache.get(key)) # using="default"
self.assertIsNone(caches["template_fragments"].get(key))
side = make_template_fragment_key("sidebar")
self.assertIsNotNone(caches["template_fragments"].get(side)) # fell back to the alias
caches["template_fragments"].delete(side)
make_template_fragment_key(fragment_name, vary_on=None) from django.core.cache.utils returns the key the tag uses, so Python code can
delete or overwrite one fragment. The keys carry a readable prefix: make_template_fragment_key("sidebar") is template.cache.sidebar. followed
by an MD5 digest of the vary_on values, and the digest changes with them. Delete from the same cache the tag used; deleting from the wrong
alias is the classic reason a fragment "refuses" to update.
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key("sidebar", [username]) # {% cache 500 sidebar username %}
cache.delete(key) # invalidates the cached fragment
The template engine itself is covered on Templates, and a different way to reuse template fragments, template partials, on Template Partials & HTMX; a partial reuses markup, it does not cache it.
The low-level cache API
When whole pages are too coarse, cache values. Any Python object that can be pickled safely can be stored: strings, dictionaries, lists of
model instances. Reach the caches through django.core.cache:
-
cachesis a dict-like object indexed by alias (caches["myalias"]); an unknown alias raisesInvalidCacheBackendError. Within one thread the same alias returns the same object, and each thread gets its own instance of the backend. -
cacheis the shortcut forcaches["default"].
| Method | Behaviour |
|---|---|
|
Store a value. |
|
Return the value, or |
|
Store only if the key is absent; returns |
|
Return the value; on a miss store |
|
One round trip for several keys. |
|
Remove keys. |
|
Remove everything from the cache, including keys your code did not set. On a shared Redis or Memcached that is almost never what you want. |
|
Give an existing key a new expiry; returns whether the key existed. |
|
Change a numeric value; |
|
Check for a key without fetching it. |
|
Move one key to the next or previous version (see Keys, prefixes and versions). |
|
Close the connection if the backend has one; otherwise a no-op. |
The first of the two patterns in the scratch project is the cache-aside idiom in one call: get_or_set runs the callable only when the key is
missing.
from django.core.cache import cache
from .models import Book
BOOK_COUNT_KEY = "book-count"
def book_count():
"""Cache-aside with get_or_set(): the callable runs only on a miss."""
def compute():
return Book.objects.count()
return cache.get_or_set(BOOK_COUNT_KEY, compute, timeout=60 * 10)
Source: Django: cache.get_or_set()
The second does it by hand, with a sentinel so that a cached None (a lookup that found nothing) counts as a hit; this is negative caching
and stops repeated misses from hammering the database:
def get_book_title(pk):
"""Cache-aside by hand, with a sentinel so that a cached None is a hit."""
sentinel = object()
key = f"book-title:{pk}"
title = cache.get(key, sentinel)
if title is sentinel:
title = Book.objects.filter(pk=pk).values_list("title", flat=True).first()
cache.set(key, title, 60) # None is cached too
return title
Source: Django: cache.get()
The tests confirmed the semantics one by one: add returns False for an existing key, incr/decr raise ValueError on a missing key,
get_many omits keys that are absent, timeout=0 stores nothing and timeout=None stores forever, expiry is honoured, and the second call to
get_book_title(999) ran with assertNumQueries(0). They also pinned the thread rule from the documentation:
def test_thread_gets_own_instance(self):
seen = {}
t = threading.Thread(target=lambda: seen.update(other=caches["default"]))
t.start(); t.join()
self.assertIsNot(seen["other"], caches["default"])
self.assertIs(caches["default"], caches["default"])
Source: Django: Accessing the cache
Three cautions for the API. First, the value you get is a copy: the backends pickle on the way in, so mutating the returned list does not
change the cached one; call set again. Second, there is no built-in protection against a cache stampede, many workers recomputing the same
expired value at once; get_or_set does not lock. A common mitigation, which is a pattern and not a documented Django feature, is to take a short
lock with cache.add("lock:name", "1", 30) (it returns False for every caller but the first) and have the others wait or serve slightly stale
data; the scratch test checks only that add behaves that way. Third, never cache anything secret for a long time or under a guessable key on a
cache that other tenants can reach.
Keys, prefixes and versions
The key you pass is not the key that reaches the backend. Django combines three parts, joined by colons by default:
def make_key(key, key_prefix, version):
return "%s:%s:%s" % (key_prefix, version, key)
Source: Django: Cache key transformation
-
KEY_PREFIXkeeps several Django instances, or production and development, from reading each other’s values on one shared cache. If the format of the cached data differs between servers the bugs are hard to diagnose, so give every instance its own prefix. In the scratch projectcache.make_key("k")returnedbookshelf:1:k. -
VERSIONis a system-wide version. Every method also takes aversionargument, so one key can be set or read at a particular version:>>> cache.set("my_key", "hello world!", version=2) >>> cache.get("my_key") # default version is 1 >>> cache.get("my_key", version=2) 'hello world!' >>> cache.incr_version("my_key", version=2) >>> cache.get("my_key", version=3) 'hello world!'Version bumps are the targeted alternative to
clear()when you change the shape of cached data in a deployment: the old entries are simply never read again and expire on their own. The scratch test exercised exactly this sequence. -
KEY_FUNCTIONis a dotted path to a function with the signature ofmake_key()above; use it to hash keys, or to change the layout.
Source: Django: Cache versioning
Memcached refuses keys longer than 250 characters and keys that contain whitespace or control characters. To keep code portable, the other
built-in backends warn with CacheKeyWarning for such keys instead of failing, and so a key that works on locmem in development can break on
Memcached in production. If your production backend accepts a wider range, silence the warning with warnings.simplefilter("ignore",
CacheKeyWarning) in the management module of an installed app, or override validate_key() in a subclass of the backend and point BACKEND
at it. Build keys from stable identifiers (f"book-title:{pk}"), never from user input of unbounded length, and hash anything long.
Source: Django: Cache key warnings
The async cache API
Since Django 4.0 BaseCache has an async twin of every method, named with an a prefix and taking the same arguments: aget, aset, aadd,
aget_or_set, aget_many, aset_many, adelete, adelete_many, aclear, atouch, aincr, adecr, ahas_key, aincr_version,
adecr_version and aclose. Use them in async def views, so the call does not block the event loop thread:
from django.core.cache import cache
from django.http import JsonResponse
async def async_count(request):
count = await cache.aget_or_set("async-hits", 0, 60)
await cache.aset("async-hits", count + 1, 60)
return JsonResponse({"previous": count, "has_key": await cache.ahas_key("async-hits")})
Source: Django: Asynchronous support
The curl run against runserver printed {"previous": 0, "has_key": true} and then {"previous": 1, "has_key": true}, and an
AsyncClient test also covered aclear, aset_many, aget_many, aincr and adelete. Be clear about what "async" means here. The 6.1
documentation says Django has developing support for asynchronous cache backends and does not yet support truly asynchronous caching. In the
installed 6.1.1 source the default async methods in BaseCache simply run the sync method through sync_to_async(…, thread_sensitive=True),
so the event loop is free while the cache call waits, at the price of a thread hop per call. The API will not change when native async backends
arrive, so write the await form now. The decorators cache_control and never_cache also accept async def views (since Django 5.0). The
general rules for async code are on the async page of this section.
Downstream caches and the Vary header
Caching also happens outside your code: a proxy such as Squid, a CDN, an ISP cache (not possible under HTTPS) and, always, the visitor’s browser.
You do not control them, but you can send hints in HTTP headers. The danger is that a cache that keys only on the URL can hand one visitor
another visitor’s page, such as a logged-in inbox. The Vary response header lists the request headers that a cache must take into account
when it builds its key. By default Django’s own cache builds keys from the fully qualified URL, so any variation by cookie, language or
User-Agent must be declared.
from django.http import HttpResponse
from django.utils.cache import patch_vary_headers
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie, vary_on_headers
@cache_page(60 * 15)
@vary_on_headers("X-Client")
def by_client(request):
return HttpResponse(f"client={request.headers.get('X-Client', '-')}")
@cache_page(60)
@vary_on_cookie
def by_cookie(request):
return HttpResponse("per visitor")
def manual_vary(request):
response = HttpResponse("manual")
patch_vary_headers(response, ["Cookie"])
return response
Source: Django: Using Vary headers
vary_on_headers("User-Agent", "Cookie") varies on every combination of the listed headers; the names are case-insensitive. vary_on_cookie
is shorthand for vary_on_headers("Cookie"). The decorators add to an existing Vary header, which is why they are preferable to setting
response.headers["Vary"] by hand, which would overwrite what other code put there. patch_vary_headers(response, […]) is the same helper for
code that builds the response itself. The tests confirmed that the X-Client view renders twice for two different header values (and once for a
repeat), and that the other two add Cookie to Vary.
Pitfall: varying on Accept-Language
With USE_I18N on, Django already puts the active language into the cache key, so it does not add the raw Accept-Language value a second time;
the source of django.utils.cache marks it redundant. The consequence is easy to miss: the "active language" is only per request when
LocaleMiddleware activates it. In an early version of the scratch project with no LocaleMiddleware, a view decorated with
@vary_on_headers("Accept-Language") returned the English page to a visitor who sent Accept-Language: es. After adding
django.middleware.locale.LocaleMiddleware after SessionMiddleware (and a LANGUAGES list), the curl run showed Content-Language: es and a
Spanish-keyed entry. If you rely on a cache that varies by language, enable the locale middleware first; internationalisation as a whole is on its
own page of this section.
Authorization
Django’s cache middleware also adds Authorization to Vary for requests that carry that header. Strictly this is not needed, because RFC 9111
lets a cache avoid reusing authenticated responses, but it simplifies keys. The documentation marks this as changed in Django 6.0.6; before
it, UpdateCacheMiddleware did not vary on Authorization. The scratch test sent Authorization: Bearer x through the per-site middleware and found
Authorization in the response’s Vary.
Controlling downstream caches: Cache-Control
A visitor meets two kinds of cache: the browser’s own, which is private, and a shared one belonging to a provider or a CDN, which is public.
Some data must never reach a shared cache. Django’s decorators from django.views.decorators.cache set the matching headers:
from django.http import HttpResponse
from django.views.decorators.cache import cache_control, never_cache
@cache_control(private=True, max_age=120)
def private_page(request):
return HttpResponse("private to this browser")
@cache_control(public=True, max_age=3600, stale_while_revalidate=60)
def public_page(request):
return HttpResponse("public")
@never_cache
def account(request):
return HttpResponse("never cached")
cache_control accepts any valid Cache-Control response directive as a keyword (no_transform=True, must_revalidate=True,
stale_while_revalidate=seconds, no_cache=True, …). private and public are mutually exclusive, and the decorator removes the other one.
Use django.utils.cache.patch_cache_control() for the same effect inside a view, for instance to mark anonymous responses public and
signed-in responses private (always together with vary_on_cookie). never_cache adds headers so that browsers and shared caches do not
store the response. The tests asserted these exact results:
private_page -> Cache-Control: private, max-age=120
public_page -> contains public and stale-while-revalidate=60
account -> Cache-Control: max-age=0, no-cache, no-store, must-revalidate, private
Even without any server-side cache, @cache_control(max_age=3600) tells clients to reuse a response for an hour. If the cache middleware is also
on, its max-age comes from CACHE_MIDDLEWARE_SECONDS and the decorator’s value takes precedence, with the headers merged correctly. For
validation-based HTTP caching (ETag and Last-Modified, answered with 304 Not Modified), see the conditional view processing topic in the
references. The cache_control directives are listed in the IANA registry; not all apply to responses.
Cache middleware order, summarised
| Rule | Why |
|---|---|
|
It runs last in the response phase and must see the complete |
|
It runs in the request phase and needs those middleware to have run before it builds the key. |
Put |
They add |
Source: Django: Order of MIDDLEWARE
Invalidation: knowing when the cache is wrong
Everything above is the easy half. The cache is a copy, and a copy goes stale. Django offers no automatic dependency tracking between your models and the cache, so you choose a strategy per value. In rough order of preference:
-
Expire quickly. A timeout of a few minutes bounds the damage of every other mistake and needs no code. Choose it by asking how stale a visitor may see this value, not how often it changes.
-
Delete on change. When the source changes, delete the keys that depend on it, with
cache.delete(),delete_many()or, for a fragment,delete(make_template_fragment_key(…)). -
Version the key. Include something that changes with the data in the key (a
modifiedtimestamp, a content hash), or bumpVERSIONor one key’s version withincr_version. Old entries are never read again and age out. This is also how you deploy a code change that alters the cached format. -
Clear everything with
cache.clear()only for a single-purpose cache; on a shared server it deletes other applications' data.
Delete-on-change belongs where changes happen. A signal receiver on post_save and post_delete is the one place that sees every write, and the
delete is scheduled with transaction.on_commit() so that a concurrent reader cannot re-cache the old rows between your write and the commit:
from django.core.cache import cache, caches
from django.core.cache.utils import make_template_fragment_key
from django.db import transaction
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from .models import Book
def invalidate_book_caches(pk):
cache.delete_many([BOOK_COUNT_KEY, f"book-title:{pk}"])
# {% cache 300 sidebar %} has no vary_on arguments and, with no using=, is
# stored in the "template_fragments" cache because that alias exists
caches["template_fragments"].delete(make_template_fragment_key("sidebar"))
@receiver(post_save, sender=Book)
@receiver(post_delete, sender=Book)
def book_changed(sender, instance, **kwargs):
# Invalidate only once the surrounding transaction has committed, so a
# concurrent reader cannot re-cache the old state in between.
transaction.on_commit(lambda: invalidate_book_caches(instance.pk))
The receiver must be connected when Django starts, so the app’s ready() imports the module (from . import services). The test that proves it
uses TransactionTestCase, because a plain TestCase wraps each test in a transaction that never commits and so never runs on_commit hooks:
class InvalidationTests(TransactionTestCase):
def test_signal_invalidates_after_commit(self):
self.assertEqual(services.book_count(), 0)
Book.objects.create(title="A")
self.assertEqual(services.book_count(), 1)
Book.objects.all().delete()
self.assertEqual(services.book_count(), 0)
Source: Django: TransactionTestCase
Know the limits of this pattern. Signals do not fire for QuerySet.update(), bulk_create() or bulk_update(), nor for raw SQL, so those need an
explicit invalidation call. A page cached with cache_page or the per-site middleware has no key your code knows (it is made of URL, headers and
prefixes), which is a strong reason to prefer short timeouts for those, or to use key_prefix and bump it. The signals themselves, transactions and
on_commit are covered on "Transactions, Raw SQL & Multiple Databases".
Testing code that caches
Tests that touch the cache must not depend on what an earlier test left behind. Three habits keep them honest:
-
Call
cache.clear()insetUp. With locmem it is local to the test process; do not do this against a shared server. -
Use
override_settings(CACHES=…)to switch a test to a temporary file cache or toDummyCachewhen the cache must be disabled. -
Prove a hit, not just a result: count how many times the expensive function ran (the scratch project keeps a counter) or use
assertNumQueries.
The scratch suite ran all of the above in a single command, with this result:
python manage.py test
Ran 28 tests in 0.023s
OK
Source: Django: Testing tools
The same suite also ran the file backend (in a temporary directory), the database backend (after createcachetable), the dummy backend and the
culling behaviour of locmem with MAX_ENTRIES of 6 and CULL_FREQUENCY of 2. Testing as a topic has its own page in this section.
Pitfalls
-
Caching personal pages without
Vary: Cookieorprivate, so one visitor’s page is served to another (Whatcache_pagedoes not do). -
Applying
cache_pageto a view with a CSRF form withoutcsrf_protectinside it. -
Using the local-memory cache with several workers and expecting shared state; or forgetting that every test process starts empty.
-
Trusting a cache to hold data. Every backend may lose any key at any time, and nothing here is permanent storage.
-
Using
cache.clear()on a Redis or Memcached shared with other applications. -
Putting the file cache inside
MEDIA_ROOT,STATIC_ROOTor a static directory, or leaving it world-writable (the data is pickled). -
Keys with whitespace or more than 250 characters, which warn on locmem and file caches and fail on Memcached.
-
Caching a function result that includes a user’s identity under a key that does not.
-
Deleting a template fragment from a different cache alias than the one the tag used.
-
Assuming
incris atomic on every backend, or thatget_or_setserialises concurrent computation. -
Forgetting
LocaleMiddlewarewhen varying by language (Pitfall: varying onAccept-Language). -
Setting a high
TIMEOUTand no invalidation, then discovering that a bug fix "did not deploy" because the cached output is still served. -
Treating a replicated Redis list in
LOCATIONas read-your-writes consistent.
Where the books are outdated
The four books consulted for this section were written for Django 1.11 to 3.x, and each treats caching briefly or by way of an example. These are the points where a reader following them today would go wrong, as of 2026-10-03:
-
Memcached. The books use
python-memcachedthroughdjango.core.cache.backends.memcached.MemcachedCache. That class was deprecated in Django 3.2 and removed in 4.1. UsePyMemcacheCache(added in 3.2) orPyLibMCCache. -
Redis. Before Django 4.0 you needed the third-party
django-redispackage. Django 4.0 added a built-inRedisCache, which is usually the better choice now; the minimum redis-py rose to 3.4.0 in 4.2, and the 6.0 notes list redis-py 5.1.0 as the first release confirmed on Python 3.12. -
Async. The books know nothing of the cache API’s async twins (
aget,aset, … since 4.0), which fit async views (since 3.1) under ASGI. -
cache_pageandCache-Control. Older text does not tell you that thecache_pagetimeout takes precedence over a view’s ownmax-age(3.1). -
Cached data after an upgrade. Cache keys changed in 3.1 (fragments and
cache_page), 3.2 (no language in the key whenUSE_I18Nis off andUSE_L10Nis on) and 6.1 (keys that vary on extra headers or arguments), so the first request after such an upgrade is a cache miss. Old material that talks of "warming up" the cache after a deployment still describes a real effect; clear or let it age out deliberately. -
Per-process thinking. Tutorials that use the default
LocMemCachein examples seldom mention that it is per process. Do not carry that into a multi-worker deployment.
What changed recently
| Version | Change |
|---|---|
3.1 |
The keys used by the |
3.2 |
|
4.0 |
The built-in |
4.1 |
|
4.2 |
Minimum redis-py raised to 3.4.0. |
5.0 |
|
6.0 |
redis-py 5.1.0 is the first release confirmed for Python 3.12, the new minimum. In the 6.0 series, 6.0.6 made |
6.1 |
Cache keys that vary on extra arguments ( |
On 5.2 LTS the same code works, except for the 6.1-only key and DatabaseCache changes above and the Authorization vary, which is documented as arriving in 6.0.6 (check the 5.2 documentation for your patch release). As of 2026-10-03 the documentation still describes native asynchronous cache backends as future work.