# NGINX caching: what gets stored, what gets served, and the gap where user data leaks

> Whether a response is stored and whether a later request is served from it are different questions with different answers. NGINX protects you from caching a cookied response, and does not protect you from serving a cached response to a cookied request. That asymmetry is where one user receives another user's page.

Source: https://ronutz.com/en/learn/nginx-proxy-cache-what-gets-stored  
Updated: 2026-07-27  
Related tools: https://ronutz.com/en/tools/nginx-proxy-cache-decision, https://ronutz.com/en/tools/nginx-proxy-pass-rewriter

---

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.
