Two questions get treated as one, and they are not. Whether NGINX stores a response is decided by the response. Whether a later request is served that stored copy is decided by the request and the cache key. Most caching accidents live in the gap.

Nothing is cached until you say so

proxy_cache naming a zone is the switch. Without it every other cache directive in the file is inert, and this is by far the most common reason nothing is ever cached — the configuration looks complete because it has proxy_cache_valid and a path, but the switch was never thrown.

What stops a response being stored

The method. proxy_cache_methods defaults to GET and HEAD. A POST is never cached unless you add it deliberately, and adding it deliberately is rarely what you want.

Set-Cookie in the response. NGINX will not store a response that sets a cookie, on the assumption it is user-specific. This default is doing real work: it is why a login page or a personalised dashboard is never cached by accident.

The origin's own instruction. Cache-Control: no-cache, no-store or private, or an Expires in the past, prevent storage. The application is telling you something about the response, and by default NGINX listens.

No lifetime. If nothing gives the status a lifetime — no matching proxy_cache_valid, no freshness header from upstream — there is nothing to store it under.

proxy_no_cache with a non-empty, non-zero value.

The two directives people swap

proxy_no_cache prevents the write. The response is fetched and used, and nothing is stored.

proxy_cache_bypass skips the lookup. The request goes upstream instead of being served from cache — and the result is still stored.

They read like synonyms and they are opposites in the half that matters. Using bypass where you meant no_cache fills the cache with exactly the responses you were trying to keep out of it.

The key decides what "the same request" means

The default key is $scheme$proxy_host$request_uri, and $request_uri includes the query string. So ?page=2 is a distinct entry, which is right — and also means any cache-busting parameter defeats the cache entirely, which is sometimes a surprise on the bill rather than in the logs.

Build a key from $uri instead and you have dropped the query string. Two requests differing only in their query now share one entry, and one of them receives the other's response.

The asymmetry that leaks

Here is the part worth reading twice.

NGINX excludes cookied responses from storage. It does not exclude cookied requests from being served.

A request arriving with Cookie: session=abc is looked up under the ordinary key. If a shared entry exists, that user is served the shared entry. NGINX never considered their cookie, because the key did not mention it.

On its own that is usually harmless, because the personalised responses were never stored. It stops being harmless the moment someone adds proxy_ignore_headers Set-Cookie — often to make caching "work" on a site where the backend sets a session cookie on every response.

Now per-user responses are stored under a key every user shares. The first logged-in user's page is served to everybody. This is not an exotic attack; it is a two-line configuration change that looks like a performance fix.

If responses vary per user, the key must vary per user$cookie_session in the key, or the responses kept out of the cache entirely. There is no third option where you ignore Set-Cookie and stay safe.

What to check when caching misbehaves

Nothing cached at all? Look for proxy_cache first, then the method, then whether anything supplies a lifetime.

Cached when it should not be? Look for proxy_ignore_headers, and read the upstream's Cache-Control to see what it asked for.

Wrong content served? Compare the cache key against everything the response actually varies on. The key is the definition of "the same request", and if it omits something that matters, the cache is behaving correctly and the definition is wrong.

What a learner should be able to state cold

proxy_cache naming a zone is what turns caching on; without it nothing else applies. Only GET and HEAD are cached by default. A response with Set-Cookie, or with Cache-Control: no-cache, no-store or private, is not stored unless those headers are ignored, and a status with no lifetime is not stored at all. proxy_no_cache prevents the write while proxy_cache_bypass skips the lookup and still writes. The key defines what counts as the same request, defaults to including the query string, and must include anything the response varies on. NGINX excludes cookied responses from storage but does not exclude cookied requests from being served, so ignoring Set-Cookie without varying the key by cookie serves one user's page to everyone.