In Django's MIDDLEWARE setting, why must UpdateCacheMiddleware come first and FetchFromCacheMiddleware come last?
answer
- request phase top-down
- response phase bottom-up
- who adds Vary headers
- store last, look up last
basics
~20 sDjango runs middleware top-down on requests and bottom-up on responses. UpdateCacheMiddleware must be first so it stores the response after every other middleware has added its Vary headers; FetchFromCacheMiddleware must be last so lookups see the same request state.
solid answer
~40 sThe site-wide cache is split in two because it works in both phases. `UpdateCacheMiddleware` acts on the **response**, and responses pass through middleware in **reverse** order, so putting it at the top of `MIDDLEWARE` makes it run **last**, after `SessionMiddleware` has added `Vary: Cookie`, `LocaleMiddleware` `Vary: Accept-Language` and `GZipMiddleware` `Vary: Accept-Encoding`. It then learns a key that includes those headers. `FetchFromCacheMiddleware` acts on the **request**, so it goes at the bottom to run after the middleware that shape the request (for example `LocaleMiddleware` setting the language). Put `UpdateCacheMiddleware` lower and it stores a page before `Vary: Cookie` exists, so one user's page is served to others. Pages default to `CACHE_MIDDLEWARE_SECONDS` (600), stored in `CACHE_MIDDLEWARE_ALIAS`.
code
python · 16 lines# settings.py
MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
]
CACHE_MIDDLEWARE_ALIAS = "default"
CACHE_MIDDLEWARE_SECONDS = 300
CACHE_MIDDLEWARE_KEY_PREFIX = "events"go deeper
Recall that the update half goes first and the fetch half goes last in MIDDLEWARE.
Explain the onion order: request phase top-down, response phase bottom-up, and which middleware add to Vary.
Diagnose leaked pages caused by a misplaced update half, and verify keys with logged-in and anonymous requests.
Decide whether site-wide caching fits at all once sessions and personalisation are in play, versus per-view or fragment caching.
## Two phases, two halves Django applies the classes in `MIDDLEWARE` like layers of an onion: - In the **request phase**, a request passes through the list **top to bottom** before reaching the view. - In the **response phase**, the response passes back **bottom to top**. The site-wide cache needs to act in both phases: once to look for a stored page before the view runs, and once to store the finished response. Django therefore ships it as two classes in `django.middleware.cache`: | Class | Phase | Job | Position | |---|---|---|---| | `FetchFromCacheMiddleware` | request | Look up a stored page for GET/HEAD and return it on a hit | **Last** in `MIDDLEWARE` | | `UpdateCacheMiddleware` | response | Store a cacheable response and learn its key | **First** in `MIDDLEWARE` | The docs admit it looks backwards: "No, that's not a typo." ## Why UpdateCacheMiddleware goes first Several built-in middleware classes **add to the `Vary` header** on the way out: - `SessionMiddleware` adds `Cookie` when the session was accessed, - `LocaleMiddleware` adds `Accept-Language`, - `GZipMiddleware` adds `Accept-Encoding`, - `CsrfViewMiddleware` adds `Cookie` when a CSRF token was used. `UpdateCacheMiddleware` reads the response's `Vary` header to decide which request headers belong in the cache key. Being first in the list means it is **last** in the response phase, so every one of those additions is already present. If it sat below `SessionMiddleware`, it would store a page for a logged-in user under a key that ignores the `Cookie` header, and the next visitor would receive that page. ## Why FetchFromCacheMiddleware goes last On the way in, `FetchFromCacheMiddleware` has to compute the same key that the update half computed. Placing it at the bottom means it runs after every middleware that alters the request in ways the key depends on, for example `LocaleMiddleware` setting `request.LANGUAGE_CODE`, which Django appends to page keys when `USE_I18N` is on. On a hit it returns the stored response straight away, so the view and the middleware beneath it are skipped. ## What happens on a hit and on a miss On a **hit**, `FetchFromCacheMiddleware` returns the stored response from its request phase. Django then runs the response phase of the middleware **above** it, including `UpdateCacheMiddleware`, which sees that the request was flagged as already served and does not store it again. The view, and the request phase of anything below the fetch half, never run. On a **miss**, the fetch half flags the request as a candidate for storing and lets it continue. The view runs, every middleware adds its headers on the way out, and finally the update half decides whether the response is cacheable and stores it. This is also why the pair is cheap to add but hard to reason about: a hit bypasses code that some teams assume always runs, such as a view that records page views or checks a feature flag. ## The related settings - **`CACHE_MIDDLEWARE_SECONDS`** (default `600`): lifetime when the response has no `max-age`. A response with `max-age=0` is not stored at all. - **`CACHE_MIDDLEWARE_ALIAS`** (default `"default"`): which `CACHES` alias stores pages. - **`CACHE_MIDDLEWARE_KEY_PREFIX`** (default `""`): an extra namespace for page keys, combined with the alias's `KEY_PREFIX`. ## The single-class alternative `django.middleware.cache.CacheMiddleware` combines both halves in one class. It only works when no other middleware needs to influence the key, because a single position cannot be both first in the response phase and last in the request phase. The docs name `LocaleMiddleware` as the usual reason to use the pair instead. `cache_page` is built on `CacheMiddleware` wrapped around one view, which is why it runs inside all middleware and misses their `Vary` additions. ## Order checklist 1. `UpdateCacheMiddleware` at the very top. 2. Everything else, including sessions, locale, CSRF, authentication and GZip. 3. `FetchFromCacheMiddleware` at the very bottom. 4. Confirm with a logged-in and an anonymous request that responses carry `Vary: Cookie` and are keyed separately, or better, that per-user pages are not cached at all.
- What goes wrong if UpdateCacheMiddleware is placed just below SessionMiddleware?In the response phase it now runs before `SessionMiddleware`, so the response it stores has no `Vary: Cookie` yet. The learned key ignores cookies, and a page rendered for one logged-in member is later served to anonymous visitors and other members requesting the same URL.
- When is the single CacheMiddleware class enough?When no other middleware needs to affect the page key, for example a site with no sessions, no locale switching and no compression middleware. As soon as something like `LocaleMiddleware` adds to `Vary` or changes the request, you need the two halves at opposite ends of `MIDDLEWARE`.
- Which timeout does the site-wide cache use for a response that sets its own max-age?The response's `max-age` wins over `CACHE_MIDDLEWARE_SECONDS` (default 600). A `max-age` of 0 means the middleware does not store the page at all. Only `cache_page`'s own timeout overrides a view's `max-age`.
The update half is the photographer at the exit who takes the picture only after everyone has added their badge; the fetch half is the doorman at the innermost door who checks the same badges before letting anyone reach the kitchen.
saying these in an interview costs you the question
- Middleware runs top-down in both the request and response phases.
- The order of the two cache middleware classes does not matter.
- FetchFromCacheMiddleware should be first so hits skip all other middleware.
- CacheMiddleware is always a drop-in replacement for the pair.
- CACHE_MIDDLEWARE_SECONDS overrides any max-age the view sets.