skip to content

In Django, how do never_cache and patch_cache_control let a view opt out of or tune the page cache, and which responses are never stored?

level: middleimportance: should knowfreq 35%

answer

  1. headers are the opt-out signal
  2. private, no-cache, no-store
  3. status and streaming checks
  4. max-age tunes the lifetime

basics

~20 s

Django's page cache reads the response headers: never_cache adds private, no-cache and no-store, which UpdateCacheMiddleware and cache_page refuse to store. patch_cache_control edits Cache-Control, for example max-age to set the lifetime; non-200, streaming and Vary: * responses are never stored.

solid answer

~30 s

The page cache decides after the view runs, by inspecting the response. `never_cache` (in `django.views.decorators.cache`) calls `add_never_cache_headers()`, which sets `Expires` to now and `Cache-Control: max-age=0, no-cache, no-store, must-revalidate, private`; responses naming `private`, `no-cache` or `no-store` are skipped. `django.utils.cache.patch_cache_control(response, **kwargs)` adds or replaces directives, turning `max_age=300` into `max-age=300`; with the middleware that `max-age` becomes the stored lifetime instead of `CACHE_MIDDLEWARE_SECONDS`, and `max-age=0` means not stored. Independently of headers, Django stores only GET/HEAD responses with status 200, never streaming responses, never `Vary: *`, and never a response that sets a cookie while varying on `Cookie`. `cache_page`'s own timeout overrides `max-age` for storage.

code

python · 15 lines
python
from django.shortcuts import render
from django.utils.cache import patch_cache_control
from django.views.decorators.cache import never_cache


def upcoming_events(request):
    response = render(request, "events/upcoming.html")
    # Under the site-wide cache middleware: store for 60 seconds.
    patch_cache_control(response, max_age=60, public=True)
    return response


@never_cache
def my_tickets(request):
    return render(request, "events/my_tickets.html")

go deeper

for a junior

Recall that never_cache keeps a view's response out of caches and that only successful GET pages are stored.

for a middle

Explain the headers never_cache writes, how patch_cache_control merges directives, and how max-age sets the stored lifetime.

for a senior

Mark every personal view defensively and know the store step's refusal rules, including Vary: *, cookies and Authorization.

for a principal

Make cacheability an explicit property of each view, reviewed like permissions, rather than an emergent result of headers.

## The page cache decides from the response Both forms of Django's whole-page cache, the `UpdateCacheMiddleware`/`FetchFromCacheMiddleware` pair and the `cache_page` decorator, make the "store or not" decision **after** the view has returned, by reading the response. That makes headers the natural way for a view to say "not me" or "only briefly". ## The rules the store step applies As of Django 6.1, the store step returns without caching when any of these hold: 1. The request was not GET or HEAD (the fetch step marks it as not to be stored). 2. The response is **streaming** (`StreamingHttpResponse`, `FileResponse`). 3. The status is not `200`. A `304` gets headers patched but is not stored. 4. The response **sets a cookie and varies on `Cookie`**. 5. `Cache-Control` contains **`private`**, **`no-cache`** or **`no-store`**, in any letter case and including qualified forms like `private="Set-Cookie"`. 6. `Vary` contains **`*`**. 7. For the middleware, the response's `max-age` is `0`. When the request carried an `Authorization` header and the response is not marked `public`, Django adds `Vary: Authorization` before storing, so different credentials get different keys. ## never_cache `django.views.decorators.cache.never_cache` wraps a view (sync or async) and calls `django.utils.cache.add_never_cache_headers()` on its response, which: - sets `Expires` to the current time, - sets `Cache-Control` to `max-age=0, no-cache, no-store, must-revalidate, private`. Rule 5 then keeps it out of Django's page cache, and the same headers tell browsers and shared caches not to keep it. Typical uses: account pages, checkout, anything behind login, and the member view of the events calendar. ## patch_cache_control `django.utils.cache.patch_cache_control(response, **kwargs)` edits the `Cache-Control` header in place: - keyword names become directives, with underscores turned into hyphens (`max_age=300` becomes `max-age=300`, `no_store=True` becomes `no-store`), - `True` values produce a bare directive, - if a `max-age` is already present and a new one is passed, the **smaller** wins, - passing `public` removes an existing `private`, and vice versa. The `cache_control(**kwargs)` decorator is a thin wrapper that calls it on a view's response. The meaning of each directive for browsers and proxies is HTTP's business; for Django's own page cache, what matters is that `max-age` sets the stored lifetime under the middleware, and `private`, `no-cache` or `no-store` stop storage entirely. ## Which timeout wins | Situation | Stored lifetime | |---|---| | Middleware, response has no `max-age` | `CACHE_MIDDLEWARE_SECONDS` (default `600`) | | Middleware, response has `max-age=N` | `N` seconds | | Middleware, `max-age=0` | Not stored | | `cache_page(T)` | `T` seconds, regardless of `max-age` | When a stored response is served, the fetch step also sets an `Age` header estimating how long it has been in the cache. ## A worked example An events site runs the site-wide pair with `CACHE_MIDDLEWARE_SECONDS = 600`: - The public calendar sets nothing special and is stored for 600 seconds. - The "happening now" page calls `patch_cache_control(response, max_age=60)`, so it is stored for only a minute. - The ticket page for a logged-in member is decorated with `never_cache`; its response carries `private` and `no-store`, so it is never stored, whoever requests it. - The iCal export is a `StreamingHttpResponse` and is never stored, whatever its headers say. ## Practical guidance - Mark personal pages with `never_cache` even if nothing caches them today; the next person to add the site-wide middleware will thank you. - Use `patch_cache_control(response, max_age=...)` in a view to give one page a shorter life under the site-wide cache. - Do not rely on status codes alone: a personalised `200` with no headers is stored happily.

  • A view sets Cache-Control: Private by assigning the header string directly. Does Django's page cache store it?
    Not in Django 6.1: directive names are compared case-insensitively, so `Private` is recognised. Older patch releases compared case-sensitively and would store it; that was a security fix in 5.2.15 and 6.0.6. Using `patch_cache_control` or `never_cache` avoids the question because they write lowercase directives.
  • Does patch_cache_control(response, max_age=600) lengthen an existing max-age=60?
    No. When both an existing and a new `max-age` are present, `patch_cache_control` keeps the smaller one, so the header stays `max-age=60`. That rule exists because a decorator and a middleware often both patch the same response.

saying these in an interview costs you the question

  • never_cache only affects browsers, not Django's own page cache.
  • Django's page cache stores any 2xx or 3xx response.
  • patch_cache_control always replaces max-age with the new value.
  • A streaming response is cached once it has finished streaming.
  • Setting max-age=0 makes the middleware use CACHE_MIDDLEWARE_SECONDS instead.