Per-site, per-view, template fragment, and the low-level API
coreintermediateDjango gives you four places to cache, and they differ in how much of the response they store. **Per-site** caches whole pages through two middleware, and only for `GET`/`HEAD` requests that returned 200. **Per-view** does the same for one view, via `@cache_page(seconds)`. **Template fragment** caches a block inside a template with `{% cache 500 sidebar %}`, so the surrounding page stays dynamic. **The low-level API** — `cache.get()`, `cache.set()`, `cache.get_or_set()` — caches any value you choose, which is the only one of the four that can cache something that is not part of a rendered response. Go as narrow as the problem allows: the wider the level, the more of the page becomes stale at once.
Think of it as
The four levels are a ladder from "cache everything, control nothing" to "cache exactly one thing, control all of it", and the right rung is the narrowest one that removes the cost you measured. Per-site caching is tempting and almost always wrong for an application with logged-in users, because a whole page is the largest unit that can go stale and the cache key does not know about the user unless a `Vary` header makes it. Per-view narrows the blast radius to one URL. Fragment caching is usually the sweet spot for a page that is mostly cheap with one expensive region — a sidebar, a navigation tree, a leaderboard — because it leaves the personalised parts alone. And the low-level API is what you reach for when the expensive thing is not a piece of HTML at all: a computed aggregate, a response from a third-party API, a permission set. The other reason to prefer the narrow rungs is invalidation: `cache.delete("leaderboard")` is a line you can write when the leaderboard changes, while "invalidate every page that contained the leaderboard" is not a thing you can express at all.
What we're doing: Cache the one expensive region of an otherwise personalised page, at two levels, without caching the page itself.
- 4
- The personalised line is outside every cache. This is the whole reason per-site caching was not used — one greeting would have frozen the entire page for everyone.
- 5–6
- `get_or_set` does the read, the miss, the compute and the write in one call, so there is no window where two code paths disagree about whether the key exists.
- 11
- `list(...)` materialises the queryset. Caching a lazy `QuerySet` stores something that re-queries when iterated, which caches nothing and costs a pickle round trip.
- 20
- `request.user.id` as a `vary_on` value gives each user their own fragment entry. Omitting it would serve one user's navigation to everyone.
Why this works: Two narrow caches remove the two measured costs — the aggregate query and the navigation render — while every personalised byte still comes from the request. A per-view or per-site cache could not have expressed that at all.
Caching a `QuerySet` instead of its results
Wrong
Better
What you see: The cache reports hits, the code looks correct, and query counts do not drop at all — because every "hit" returns an unevaluated queryset that queries again on first iteration.
Why: A `QuerySet` is lazy: it holds a query, not rows. Pickling one into the cache stores the query, so retrieving it and iterating executes the SQL exactly as if there had been no cache — while additionally paying to serialise and deserialise. Forcing evaluation with `list()` is what makes the cached value the data rather than the intent to fetch it.
- Per-site middleware — the entire page, for every GET/HEAD that returned 200 — the largest thing that can go stale
- @cache_page on one view — one URL's full response, keyed by path and query string
- {% cache %} template fragment — one block; the rest of the page still renders per request, so personalisation survives
- cache.get_or_set() in Python — one value — an aggregate, an API response, a permission set. The only level with precise invalidation.
- The queryset itself — no cache at all — often the right answer once select_related and an index are in place
The four levels, narrowest last
Together
Remember: Four levels, from the whole site down to a single value, and the right one is the narrowest that removes the cost you measured. Per-site and per-view only cache `GET`/`HEAD` 200 responses and key on the URL — which makes per-site caching wrong for anything personalised. Fragment caching keeps the rest of the page live; the low-level API is the only level that caches a value rather than output, and the only one with precise invalidation. Always `list()` a queryset before caching it.
See also: cache backends keys and ttl · invalidation stampede and consistency · built in tags

