nginx's default proxy cache key is `$scheme$proxy_host$request_uri`. What risks does that create for a backend that serves per-user pages, and which nginx directives keep private responses out of a shared cache?
answer
- look at what the key omits
- the upstream name, not the client host
- read side and write side are separate
- cookies were never in the key
- query strings multiply entries
basics
~20 sThe default key ignores cookies and Authorization, and uses the proxied upstream rather than the client Host, so personalised pages and different virtual hosts can collide on one entry. Guard with proxy_cache_bypass and proxy_no_cache together, and extend proxy_cache_key deliberately.
solid answer
~50 sThe default key contains scheme, `$proxy_host` and the request URI — no cookies, no `Authorization`, and notably not `$host`. Two consequences. First, two server blocks proxying to the same upstream hash the same path to the same entry, so one site's page can be served under the other; add `$host` to `proxy_cache_key` whenever a zone is shared. Second, anything personalised is cached under a key shared by every user. nginx's default refusal to store responses carrying `Set-Cookie` is the accidental safety net here, and people remove it with `proxy_ignore_headers` without realising. The explicit controls are a pair: `proxy_cache_bypass` says "do not serve this request from the cache", `proxy_no_cache` says "do not store this response". Set both on the same conditions — typically `$http_authorization` and a session cookie — because bypass alone still stores the personalised page for the next anonymous visitor.
code
nginx · 18 linesmap $http_cookie $skip_cache {
default 0;
"~*sessionid=" 1;
}
server {
server_name a.example.com;
location / {
proxy_pass http://app;
proxy_cache z;
proxy_cache_key "$scheme$host$request_uri";
proxy_cache_bypass $skip_cache $http_authorization;
proxy_no_cache $skip_cache $http_authorization;
proxy_cache_valid 200 60s;
add_header X-Cache-Status $upstream_cache_status;
}
}go deeper
Know that the cache key is scheme, upstream and request URI by default, that cookies are not part of it, and that this is why a personalised page must not be cached without extra configuration.
Explain the split between proxy_cache_bypass and proxy_no_cache — read side versus write side — and be able to write both against the same map or variable set.
Reason about the whole key surface: which virtual hosts share a zone, which query parameters change the response, and what an attacker can do by inventing parameters. Treat a wrong-user hit as an incident class, not a tuning miss.
Set the rule for the platform: what may be cached at a shared tier at all, who reviews a cache key before it ships, and how personalised and anonymous traffic are separated by URL space so a single misconfiguration cannot cross them.
## What the default key contains, and what it leaves out `proxy_cache_key $scheme$proxy_host$request_uri;` — three parts: - `$scheme` — `http` or `https` - `$proxy_host` — the name and port from the `proxy_pass` URL, i.e. the *upstream*, not the client's `Host` - `$request_uri` — the full original request line after the method, query string included What is absent matters more: no cookies, no `Authorization`, no client `Host`, no `Accept-Language`. nginx does store an upstream `Vary` and factors the named request headers in when matching, but nothing else about the request influences the entry. ## Failure one: virtual hosts colliding ```nginx server { server_name a.example.com; location / { proxy_pass http://app; proxy_cache z; } } server { server_name b.example.com; location / { proxy_pass http://app; proxy_cache z; } } ``` Both sites hash `https` + `app` + `/pricing` to one key. Whichever host is requested first populates the entry, and the other host is served that page. The fix is one line — put the client host in the key: ```nginx proxy_cache_key "$scheme$host$request_uri"; ``` This is worth doing by default in any multi-tenant configuration, because the failure is silent and reproduces only under real traffic. ## Failure two: personalised pages in a shared cache A page that renders "Welcome back, Dana" stored under a key that ignores identity is served to every subsequent visitor. Normally nginx protects you by accident: the framework sets a session cookie, the response carries `Set-Cookie`, and nginx refuses to store it. The moment someone adds `proxy_ignore_headers Set-Cookie` — usually to make caching "finally work" — that protection disappears, and the stored copy carries the original user's `Set-Cookie` too, so later clients are handed a live session identifier unless `proxy_hide_header Set-Cookie` strips it. The deliberate controls are two directives that look interchangeable and are not: - **`proxy_cache_bypass`** — evaluate the listed variables; if any is non-empty and not `0`, do not take the response *from* the cache. The request goes upstream and the fresh response may still be **stored**. - **`proxy_no_cache`** — same evaluation; if it fires, the response is **not saved**. Use them together on identical conditions: ```nginx map $http_cookie $skip_cache { default 0; "~*sessionid=" 1; } location / { proxy_pass http://app; proxy_cache z; proxy_cache_key "$scheme$host$request_uri"; proxy_cache_bypass $skip_cache $http_authorization; proxy_no_cache $skip_cache $http_authorization; proxy_cache_valid 200 60s; } ``` Note `$http_authorization`. nginx does not automatically exclude requests carrying an `Authorization` header from its cache; if your API is token-authenticated and the backend forgets to mark responses private, nginx will happily serve one caller's data to another. Listing it explicitly is cheap insurance. ## The third failure: key fragmentation Because `$request_uri` includes the query string, `/article?utm_source=twitter` and `/article?utm_source=newsletter` are different entries for identical content. A campaign can shatter the hit ratio overnight, and unbounded parameters let anyone fill the cache with junk keys, evicting the entries you wanted. Two responses: normalise the key so only parameters that change the response participate, or strip tracking parameters at the edge. With a small, known parameter set: ```nginx proxy_cache_key "$scheme$host$uri$arg_page$arg_sort"; ``` That is deliberately narrower than `$request_uri` — everything not named is now invisible to the cache, which is safe only if it is genuinely invisible to the backend. Getting that judgment wrong produces the exact bug this whole area is about: two different responses sharing one key. ## How to review a cache key Ask one question of every input the backend uses to build the response — host, path, query parameter, cookie, header, token — and place it into one of three buckets: *in the key* (it changes the response and has few values), *excluded and stripped* (it does not change the response), or *not cacheable at all* (it makes the response per-user). Anything you cannot confidently place belongs in the third bucket. A cache that serves the wrong user's page is a security incident, and the cheapest defence is to cache less.
- Why is proxy_cache_bypass on its own not enough to protect a personalised page?Because it only controls the read side. A logged-in request skips the lookup and goes upstream, but the personalised response that comes back is still eligible for storage under the shared key — and the next anonymous visitor gets it as a HIT. proxy_no_cache controls the write side. Setting both on identical conditions is the only configuration that is correct in both directions.
- A marketing campaign appends utm parameters to every link and your hit ratio collapses. What do you do?Each distinct query string is a distinct key, so identical content is stored many times. Either normalise the key to the parameters that actually affect the response — for example $uri plus the specific $arg_ variables you support — or strip tracking parameters before the key is computed. Narrowing the key is safe only for parameters the backend truly ignores.
- Does nginx exclude requests carrying an Authorization header from the cache automatically?No. Unlike the shared-cache rules a CDN might apply, nginx makes no special case for Authorization; if the response is storable, it is stored under a key that ignores the header. That is why nginx's own documentation shows $http_authorization as an example bypass condition. On any token-authenticated API, list it in both proxy_cache_bypass and proxy_no_cache.
saying these in an interview costs you the question
- Believes the default cache key includes cookies
- Assumes $proxy_host is the client's Host header
- Uses proxy_cache_bypass alone for logged-in users
- Ignores Set-Cookie without hiding it from clients
- Thinks nginx skips the cache for Authorization automatically