skip to content

You added `proxy_cache` to an nginx location, but $upstream_cache_status logs MISS on every single request for the same URL and the upstream still sees full load. What would you check, in order, in nginx's own rules and in the upstream's response headers?

level: middleimportance: must knowfreq 62%

answer

  1. MISS is not BYPASS
  2. the upstream gets a vote
  3. one cookie header spoils it
  4. ignoring is not the same as hiding
  5. the directive loses to the header

basics

~20 s

A permanent MISS means nginx is looking but never storing. Check the request method (GET and HEAD only by default), then the upstream headers — Set-Cookie, Cache-Control no-store/no-cache/private/max-age=0, or a past Expires all block storage — then whether proxy_cache_valid covers that status code.

solid answer

~50 s

First read the status honestly: MISS means nginx consulted the cache and went upstream; a *permanent* MISS means it never stores what comes back. So look at the storage rules, not the lookup. Start with the method — only GET and HEAD are cacheable by default. Then dump the upstream's response headers with `curl -sI` straight against the backend: `Set-Cookie` alone stops nginx storing anything, and so do `Cache-Control: no-store`, `no-cache`, `private` or `max-age=0`, an `Expires` in the past, or `X-Accel-Expires: 0`. Those response headers take priority over `proxy_cache_valid`, so adding a lifetime does not override them — `proxy_ignore_headers` does. Then check that `proxy_cache_valid` actually covers the status code you are getting, that `proxy_cache_min_uses` is not above 1, and that buffering is on, since an unbuffered response is not cached. If you ignore `Set-Cookie`, pair it with `proxy_hide_header Set-Cookie` or you will hand one user's session to everybody.

code

bash · 7 lines
bash
# 1. what does the backend actually send?
curl -sI -H 'Host: app.example.com' http://127.0.0.1:8080/catalog

# 2. does the same URL turn into a HIT on the second try?
for i in 1 2; do
  curl -s -o /dev/null -D - https://app.example.com/catalog | grep -i '^x-cache-status'
done

go deeper

for a junior

Know that nginx respects the backend's caching headers by default, and that a Set-Cookie on the response is enough to stop anything being stored. Be able to run curl against the backend and read those headers.

for a middle

Walk the checklist in order and state the precedence rule out loud: response headers beat proxy_cache_valid, and proxy_ignore_headers is what changes that. Distinguish MISS from BYPASS from EXPIRED.

for a senior

Show the safety half of the fix — ignoring Set-Cookie without proxy_hide_header leaks sessions between users — and describe how you would prove the URL is genuinely anonymous before overriding the backend at all.

for a principal

Decide where cacheability should be declared. Overriding headers at the proxy is a local patch that diverges from what the CDN and browsers see; push for correct upstream headers as the contract and treat proxy-side overrides as documented exceptions.

## Read the status first `$upstream_cache_status` distinguishes two very different failures, and skipping this step sends people down the wrong path: - **BYPASS** — nginx did not look. Some `proxy_cache_bypass` variable evaluated to a non-empty value other than `0`. The problem is in your bypass conditions. - **MISS forever** — nginx looked, found nothing, proxied, and then declined to store the result. The problem is in the storage rules. The question is the second case, so work through what forbids storage. ## 1. The request method `proxy_cache_methods` defaults to `GET HEAD`. If the endpoint is a POST — including "queries" implemented as POST because the parameters were long — nothing will ever be cached. Adding POST is possible but then the default key is unsound, since two different bodies share one URL. ## 2. The upstream's response headers This is the usual culprit, and the way to see it is to bypass nginx entirely: ```bash curl -sI -H 'Host: app.example.com' http://127.0.0.1:8080/catalog ``` Any of these makes nginx refuse to store the response: - `Set-Cookie` — any value at all. Application frameworks emit a session cookie on nearly every response, which is why a freshly-proxied app caches nothing. - `Cache-Control: no-store`, `no-cache`, `private`, or `max-age=0` - `Expires` with a date in the past (or `Expires: 0`) - `X-Accel-Expires: 0` — nginx's own control header, which takes priority over everything else - `Vary: *` ## 3. The precedence rule people get backwards `proxy_cache_valid 200 10m;` does **not** override the headers above. Parameters carried in the response header win over the directive; `proxy_cache_valid` supplies a lifetime for responses that expressed no opinion. If you have decided you know better than the backend, say so explicitly: ```nginx location /catalog/ { proxy_pass http://backend; proxy_cache api; proxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires; proxy_hide_header Set-Cookie; proxy_cache_valid 200 30s; } ``` Note the second line. `proxy_ignore_headers Set-Cookie` only stops the header from *blocking storage*; the header is still in the stored response and will be replayed to every later client. `proxy_hide_header Set-Cookie` strips it. Ignoring `Set-Cookie` without hiding it is one of the most damaging misconfigurations in this area — it hands one user's session identifier to everyone who gets the hit. Ignore it only for a URL you are certain is anonymous. ## 4. The remaining nginx-side reasons - **No matching `proxy_cache_valid`.** If the backend sends no cache headers and you never gave that status code a lifetime, nothing is stored. `proxy_cache_valid any 1m;` is a blunt catch-all worth trying while diagnosing. - **`proxy_cache_min_uses`.** Default 1. If it has been raised, the first N-1 requests for a key are deliberately not stored. - **Buffering.** Caching requires nginx to buffer the response; with `proxy_buffering off`, or an upstream sending `X-Accel-Buffering: no`, there is nothing to store. This one is easy to miss because it is usually set for an unrelated streaming endpoint and then inherited. - **Range requests.** A partial response is not cached as a whole object without the slice module; a client or CDN issuing ranged GETs can look like a cache that never fills. ## 5. Confirm, do not assume Drive the same URL twice and log the status both times: ```bash for i in 1 2; do curl -s -o /dev/null -D - https://app.example.com/catalog | grep -i x-cache-status; done ``` MISS then HIT means it works. MISS then MISS with a `Set-Cookie` visible on the direct backend call is the diagnosis. Prefer the access log over a response header for the reading — a header added at one level of the config can be replaced by another `add_header` elsewhere, while the log field is always written. Finally, once it caches, check that it caches the *right* thing: a page that is now shared between users must not be personalised, and a URL that varies by query string must have the query string in the key.

  • What is the difference between proxy_ignore_headers and proxy_hide_header when dealing with Set-Cookie?
    `proxy_ignore_headers Set-Cookie` tells nginx to disregard the header when deciding whether the response may be *stored*. The header itself remains part of the stored response and is replayed to every client that later hits it. `proxy_hide_header Set-Cookie` removes it from what nginx sends downstream. You need both, in that order of reasoning, and only on URLs you know carry nothing per-user.
  • How would you tell a BYPASS from a MISS in a real incident, and what does each point you at?
    Read $upstream_cache_status in the access log. BYPASS means a proxy_cache_bypass variable was non-empty and non-zero, so nginx skipped the lookup — inspect those conditions, typically a cookie or query argument that is set far more often than you assumed. MISS means the lookup happened and failed; if it never becomes HIT, the response is not being stored, so investigate methods and response headers.
  • The backend sends Cache-Control: private on HTML that is genuinely identical for every visitor. Would you override it in nginx, and how?
    Only after confirming the claim, then yes — `proxy_ignore_headers Cache-Control` with an explicit `proxy_cache_valid`, scoped to that one location, plus hiding any Set-Cookie. But the better fix is upstream: change the application to emit an honest lifetime, so the CDN, the browser and this proxy all agree. Overriding at the proxy hides a lie rather than correcting it.

saying these in an interview costs you the question

  • Thinks proxy_cache_valid overrides upstream Cache-Control
  • Treats MISS and BYPASS as the same symptom
  • Adds proxy_ignore_headers Set-Cookie without hiding the header
  • Forgets that only GET and HEAD are cached by default
  • Assumes a 502 or 404 is cached without a matching valid rule

context