Chapter 16 · Caching, files, mail, signals and tasks
16.5 The page cache: UpdateCacheMiddleware, FetchFromCacheMiddleware and cache_page
The page cache keeps whole responses and answers a repeated "GET" or "HEAD" with one of them before the view runs. FetchFromCacheMiddleware looks a response up, UpdateCacheMiddleware stores one, CacheMiddleware is both, and cache_page applies CacheMiddleware to one view; the keys are made by learn_cache_key and get_cache_key.
A page cache must decide, before any view has run, whether it holds the answer, and the URL alone does not decide it. A response may depend on request headers, a cookie or a language, and names them in its "Vary" header; requests for one URL that differ in those headers must not share an entry. But at lookup there is no response yet to say which. So Django learns the list from each response it stores for a URL and keeps it in the cache under a key made from the URL; a later request reads the list first and builds from it the key of the page, “without building the response object itself”, as the docstring says. The cache beneath is that of The cache API: caches and BaseCache.
The two halves, and their order
The module’s docstring puts UpdateCacheMiddleware first in MIDDLEWARE and FetchFromCacheMiddleware last, counterintuitive but correct, it says: the update half must run last on the way out, where the chain runs bottom-up, and the fetch half last on the way in (django/; Handlers and middleware). So the fetch half makes its key after LocaleMiddleware has set the language, and the update half reads the "Vary" after SessionMiddleware, GZipMiddleware and LocaleMiddleware have added to it.
On a hit, FetchFromCacheMiddleware returns the stored response, and MiddlewareMixin does not call the layer inside it. With the fetch half last, the URL resolver, the process_view hooks and the view do not run. The response goes back out through the middleware above, UpdateCacheMiddleware last.
The halves speak through one attribute of the request, HttpRequest. The fetch half sets it true after a miss, and false for a hit or for a method other than "GET" and "HEAD"; the update half stores only when it is true. A hit passing back out is not stored again, and a request answered by a middleware between the two, so that the fetch half never ran, has no attribute and stores nothing.
A miss and a hit
The example’s timetable view is decorated with cache_page(60), which runs CacheMiddleware around it. Two requests from the test client two seconds apart, a "HEAD" and a "POST", with the queries counted:
GET /timetable/ 200, 1 query
Expires: Thu, 15 Oct 2026 21:10:51 GMT
Cache-Control: max-age=60
the cache's keys:
:1:views.decorators.cache.cache_header..d85587acf917224b6af36a3c98d2b75f.en-us.UTC -> []
:1:views.decorators.cache.cache_page..GET.d85587acf917224b6af36a3c98d2b75f.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC -> the response
GET /timetable/ 200, 0 queries
Expires: Thu, 15 Oct 2026 21:10:51 GMT
Cache-Control: max-age=60
Age: 2
HEAD /timetable/ 200, 0 queries, the same headers
POST /timetable/ 200, 1 query, no Expires, no Cache-Control
The first request missed and left two entries: the header list, empty since the response has no "Vary", and the response, with the "Expires" and "max-age" put on it before it was stored. The second, and the "HEAD", were answered from that entry; the "POST" was neither looked up nor stored.
Two keys for one URL
learn_cache_key runs when a response is to be stored. It turns each name in the response’s "Vary" into the request’s key in HttpRequest.META, upper case, hyphens as underscores, with "HTTP_" in front, sorts the list, stores it under the header key for the timeout it is given, and returns the page key for the request’s method. With USE_I18N on, Accept-Language is left out: a comment says the key already carries the language, and the raw header would store the same content under several keys (ticket 18191).
get_cache_key runs at lookup. It reads the list under the header key, returns None if there is none, and otherwise builds the page key for the method it is given. A header key holds one list, written again by each response stored, so the last response’s "Vary" decides how the next request for that URL, language and zone is keyed.
Both keys hash the URL from HttpRequest, host and query string included. The page key also hashes each listed header’s value, empty where the request lacks it, as a netstring: its length in bytes, a colon, the bytes and a comma. Django’s tests check that "EU" then nothing and nothing then "EU" get different keys. Then _i18n_cache_key_suffix adds, with USE_I18N, the language, the request’s attribute LANGUAGE_CODE where LocaleMiddleware has set it and otherwise the active language, and with USE_TZ the current time zone’s name (_i18n_cache_key_suffix; Choosing a language: LocaleMiddleware, Time zones). The cache puts its own prefix and version in front.
FetchFromCacheMiddleware.process_request
For a method other than "GET" or "HEAD", FetchFromCacheMiddleware sets the flag, HttpRequest, false and returns. Otherwise it asks get_cache_key for the key of a "GET", since the module expects a "HEAD" to get exactly the "GET"'s headers. With no key it sets the flag true and returns None; so it does with no entry, once a "HEAD" has tried the key of an earlier "HEAD".
On a hit, if the stored "max-age" and "Expires" both parse, it sets "Age": the first less the seconds left before the second, never below 0 in case, the comment says, the clock was turned back. It sets the flag false and returns what the cache gave back.
UpdateCacheMiddleware.process_response
UpdateCacheMiddleware returns the response untouched at the first of these that applies:
HttpRequestis missing or false.._cache_update_cache - The response is streaming, or its status is neither 200 nor 304.
- It sets cookies and its
"Vary"names Cookie; the comment calls such a cookie user-specific and maybe security-sensitive. - Its Cache-Control has
"private","no-cache"or"no-store", read in lower case bysplit_directive_names, so thatprivate="Set-Cookie"counts too. - Its
"Vary"holds*. - No page timeout is set, and its
"max-age"is 0.
The timeout is the page timeout, which only CacheMiddleware sets, or else the response’s "max-age", or else the cache timeout, CACHE_MIDDLEWARE_SECONDS unless CacheMiddleware is given one. The middleware gives it to patch_response_headers, which adds "Expires" if absent and a "max-age", keeping a smaller one already there (Cache headers and conditional requests: django/). If the request has an "Authorization" header and the Cache-Control is not "public", it adds Authorization to "Vary", so each credential has its own page key, to simplify the keys, the documentation says. Then, if the timeout is not 0 and the status is 200, it calls learn_cache_key and stores the response under the page key for the timeout. A 304 gets the headers and is not stored.
A response with a callable render is stored by a post-render callback, which runs at once if it is rendered already; a template response refuses to be pickled before it is rendered.
CacheMiddleware and cache_page
CacheMiddleware subclasses both halves, and fetches on the way in and updates on the way out from one place in the list; the module’s docstring keeps it for simple sites, the two halves being needed where another middleware “needs to affect the cache key”, most often LocaleMiddleware. CacheMiddleware takes a cache timeout, a page timeout, and a key prefix and a cache alias as keyword arguments, and a comment separates a value provided as the default from one not provided: key_prefix=None gives the prefix "", cache_alias=None the alias DEFAULT_CACHE_ALIAS, "default", and either left out keeps the setting’s value. A cache timeout replaces CACHE_MIDDLEWARE_SECONDS unless it is None.
cache_page(timeout, *, cache=None, key_prefix=None) calls decorator_from_middleware_with_args(CacheMiddleware) with page_timeout=timeout and always with the alias and the prefix. So it runs inside the whole chain, after the URL is resolved, and returns a hit without process_response (make_middleware_decorator; A middleware as a decorator). Its timeout, unless None, wins over the response’s "max-age", and it never takes the prefix or the alias from the settings:
django/conf/global_settings.py .| setting | default | for the middleware | for cache_page |
|---|---|---|---|
CACHE_ |
default 600 | for the middleware the timeout when the response has no "max-age" |
for cache_page the same, when its timeout is None |
CACHE_ |
default "" |
for the middleware the prefix in both keys | for cache_page not used: its own prefix, or "" |
CACHE_ |
default "default" |
for the middleware the cache used | for cache_page not used: its own alias, or "default" |