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

RedisCache (built in since Django 4.0) or PyMemcacheCache; both are shared by all workers.

The backends

Cache while developing, or in tests

LocMemCache (the default) or DummyCache to turn caching off without touching code.

Local memory, Dummy, and custom backends

Cache everything for anonymous traffic

UpdateCacheMiddleware first and FetchFromCacheMiddleware last in MIDDLEWARE.

The per-site cache

Cache one expensive view

@cache_page(seconds), or wrap the view in the URLconf.

The per-view cache

Cache one expensive piece of a page

The {% cache %} template tag.

Template fragment caching

Cache a query result or an API response

cache.get_or_set(key, callable, timeout).

The low-level cache API

Use the cache from an async def view

await cache.aget(…​), aset(…​), aget_or_set(…​).

The async cache API

Keep one URL from serving the wrong variant

@vary_on_headers, @vary_on_cookie, patch_vary_headers.

Downstream caches and the Vary header

Stop browsers and proxies caching private data

@cache_control(private=True) or @never_cache.

Controlling downstream caches: Cache-Control

Remove stale data after a change

cache.delete() in transaction.on_commit(), key versions, short timeouts.

Invalidation: knowing when the cache is wrong

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.

catalog/models.py
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

BACKEND

Dotted path of the cache class. A path to your own subclass is allowed, but the built-in backends are the well-tested choice.

LOCATION

Backend specific: a URL (Redis), host:port, a list of them or a unix: socket path (Memcached), a table name (database), an absolute directory (file), or an identifying name (local memory).

TIMEOUT

Default lifetime in seconds. The default is 300. None means keys never expire; 0 means "do not cache".

OPTIONS

Backend specific. For the backends that cull on their own (local memory, file, database): MAX_ENTRIES (default 300) and CULL_FREQUENCY (default 3, meaning one third of the entries are removed when the limit is hit; 0 empties the whole cache). For Redis and Memcached the options are passed as keyword arguments to the client library.

KEY_PREFIX

String prepended to every key; empty by default. Use it when several Django instances or environments share one cache server.

VERSION

Default version number combined into every key; 1 by default. See Keys, prefixes and versions.

KEY_FUNCTION

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.

config/settings.py (the cache part)
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"

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

django.core.cache.backends.redis.RedisCache

Yes

Production. Needs the redis package (redis-py); hiredis is recommended.

Memcached

django.core.cache.backends.memcached.PyMemcacheCache or PyLibMCCache

Yes

Production. Memory only, so a restart empties it.

Database

django.core.cache.backends.db.DatabaseCache

Yes

When no cache server is available and the database is fast and well indexed.

File system

django.core.cache.backends.filebased.FileBasedCache

Yes, on one host

Small deployments and development.

Local memory

django.core.cache.backends.locmem.LocMemCache

No: one private cache per process

Development and tests. It is the default when CACHES is not set.

Dummy

django.core.cache.backends.dummy.DummyCache

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:

config/settings.py
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"},
    }
}

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
],

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,
        },
    }
}

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");

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},
    }
}

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",
    }
}

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"}}

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:

config/settings.py
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

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", ""))
flowchart LR B[Browser] --> U1[UpdateCacheMiddleware
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):

catalog/views.py
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")

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:

catalog/urls.py
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.

catalog/templates/catalog/home.html
{% 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:

  • caches is a dict-like object indexed by alias (caches["myalias"]); an unknown alias raises InvalidCacheBackendError. Within one thread the same alias returns the same object, and each thread gets its own instance of the backend.

  • cache is the shortcut for caches["default"].

Method Behaviour

cache.set(key, value, timeout=DEFAULT_TIMEOUT, version=None)

Store a value. timeout=None caches forever; 0 does not cache.

cache.get(key, default=None, version=None)

Return the value, or default on a miss. Use a sentinel default to tell a stored None from a miss.

cache.add(key, value, timeout, version)

Store only if the key is absent; returns True if it stored.

cache.get_or_set(key, default, timeout, version)

Return the value; on a miss store default (or the result of calling it, when it is a callable) and return that.

cache.get_many(keys) / cache.set_many(dict, timeout)

One round trip for several keys. get_many returns only the keys that exist; with memcached set_many returns the keys that failed.

cache.delete(key) / cache.delete_many(keys)

Remove keys. delete returns whether the key existed.

cache.clear()

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.

cache.touch(key, timeout)

Give an existing key a new expiry; returns whether the key existed.

cache.incr(key, delta=1) / cache.decr(key, delta=1)

Change a numeric value; ValueError if the key does not exist. Atomic only on backends with a native operation (Memcached); elsewhere it is read-then-write.

cache.has_key(key)

Check for a key without fetching it.

cache.incr_version(key) / cache.decr_version(key)

Move one key to the next or previous version (see Keys, prefixes and versions).

cache.close()

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.

catalog/services.py
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)

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

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"])

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)
  • KEY_PREFIX keeps 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 project cache.make_key("k") returned bookshelf:1:k.

  • VERSION is a system-wide version. Every method also takes a version argument, 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_FUNCTION is a dotted path to a function with the signature of make_key() above; use it to hash keys, or to change the layout.

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.

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")})

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.

catalog/views.py
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

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

UpdateCacheMiddleware goes before everything that can add to Vary

It runs last in the response phase and must see the complete Vary header.

FetchFromCacheMiddleware goes after everything that can add to Vary

It runs in the request phase and needs those middleware to have run before it builds the key.

Put SessionMiddleware (and, on a translated site, LocaleMiddleware) between them

They add Cookie and Accept-Language to Vary; the per-site example above includes SessionMiddleware, and Pitfall: varying on Accept-Language adds LocaleMiddleware.

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:

  1. 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.

  2. 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(…​)).

  3. Version the key. Include something that changes with the data in the key (a modified timestamp, a content hash), or bump VERSION or one key’s version with incr_version. Old entries are never read again and age out. This is also how you deploy a code change that alters the cached format.

  4. 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:

catalog/services.py
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)

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() in setUp. 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 to DummyCache when 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

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: Cookie or private, so one visitor’s page is served to another (What cache_page does not do).

  • Applying cache_page to a view with a CSRF form without csrf_protect inside 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_ROOT or 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 incr is atomic on every backend, or that get_or_set serialises concurrent computation.

  • Forgetting LocaleMiddleware when varying by language (Pitfall: varying on Accept-Language).

  • Setting a high TIMEOUT and no invalidation, then discovering that a bug fix "did not deploy" because the cached output is still served.

  • Treating a replicated Redis list in LOCATION as 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-memcached through django.core.cache.backends.memcached.MemcachedCache. That class was deprecated in Django 3.2 and removed in 4.1. Use PyMemcacheCache (added in 3.2) or PyLibMCCache.

  • Redis. Before Django 4.0 you needed the third-party django-redis package. Django 4.0 added a built-in RedisCache, 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_page and Cache-Control. Older text does not tell you that the cache_page timeout takes precedence over a view’s own max-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 when USE_I18N is off and USE_L10N is 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 LocMemCache in 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 cache tag and make_template_fragment_key() changed; the cache_page timeout takes precedence over Cache-Control: max-age; cache_control() and patch_cache_control() accept several field names in no-cache.

3.2

PyMemcacheCache added; MemcachedCache deprecated; the key no longer includes the language when USE_I18N is off and USE_L10N is on; None stored in the cache is handled consistently by get_many, get_or_set, has_key, incr, decr and the version methods.

4.0

The built-in RedisCache backend; the first async methods on BaseCache (aadd, aget, aset, aget_or_set, adelete_many and the rest).

4.1

MemcachedCache removed.

4.2

Minimum redis-py raised to 3.4.0.

5.0

cache_control() and never_cache() support async views.

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 UpdateCacheMiddleware add Authorization to Vary.

6.1

Cache keys that vary on extra arguments (cache_page, UpdateCacheMiddleware, the cache tag’s vary_on and make_template_fragment_key()) differ from older releases, so the first request after upgrading is a miss; DatabaseCache validates Base64 strictly when decoding values.

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.