# Cache-Control (/en/docs/web-security/security-headers/cache-control)



`Cache-Control` is the response header that decides whether a copy of the
response may be stored and reused, by the browser's own private cache and by
shared caches (CDNs, reverse proxies, corporate proxies) that serve one stored
response to many users. Caches store and reuse by design: HTTP is built to
cache as much as possible, so even a response with no caching headers at all
can be stored and reused. This is called
[heuristic caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching).

That default is the security problem. A personalized page (an account
dashboard, an order confirmation) stored in a shared cache can be served to a
different user. This page covers only that security side, keeping sensitive
responses out of caches; performance tuning with this header is out of scope.

For a response carrying personal or session data:

```http
Cache-Control: no-store
```

## Values and what each does [#values-and-what-each-does]

| Directive            | Status  | Description                                                                                                            |
| -------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| `no-store`           | ✅ Good  | The security directive. No cache, private or shared, may store any part of the request or response.                    |
| `private`            | ✅ Good  | Shared caches must not store the response. The browser cache still can, so it is not a substitute for `no-store`.      |
| `no-cache`           | ❌ Risky | Unsafe as a confidentiality control. It means revalidate before reuse, not do not store; the bytes still land on disk. |
| `public`             | ❌ Risky | Lets any cache store the response, including some it would otherwise refuse. Never on personalized responses.          |
| `max-age=<seconds>`  | ✅ Good  | Freshness lifetime, a performance control. `max-age=0` marks the response stale immediately.                           |
| `s-maxage=<seconds>` | ✅ Good  | Freshness lifetime for shared caches only (CDN tuning); overrides `max-age` for them. Side effect below.               |
| `must-revalidate`    | ✅ Good  | A response that has gone stale must be revalidated before reuse. Side effect below.                                    |

Two defaults catch almost everyone out. First,
[`Set-Cookie`](/en/docs/web-security/security-headers/cookie-security) does not
prevent caching: [RFC 9111](https://httpwg.org/specs/rfc9111.html) states that
a cacheable response with a `Set-Cookie` header can be, and often is, used to
satisfy later requests. Second, heuristic caching means a response with no
caching headers at all may still be stored, with the cache inventing its own
freshness lifetime. Sending nothing is not a policy; it is permission.

One nuance on `s-maxage` and `must-revalidate`: shared caches must not store a
response to a request that carried an `Authorization` header, unless `public`,
`s-maxage`, or `must-revalidate` explicitly allows it. So those two directives
quietly unlock shared caching of `Authorization` responses. And that built-in
protection covers only the `Authorization` header, not cookie-based sessions,
which is how most modern applications authenticate.

## What it protects against [#what-it-protects-against]

* **Other users reading personalized pages from a shared cache.** This is not
  theoretical. On December 25, 2015, during a denial-of-service attack, a
  caching partner's configuration change incorrectly cached Steam web traffic
  for authenticated users, and around 34,000 users were served other users'
  Store pages, exposing billing addresses, email addresses, purchase
  histories, and the last digits of phone numbers and payment cards
  ([SecurityWeek's coverage](https://www.securityweek.com/details-34000-steam-users-exposed-during-ddos-attack/)).
  `no-store` (or at minimum `private`) on those responses is what prevents a
  shared cache from ever holding them.
* **Post-logout reading on shared machines.** On a library or office computer,
  the next person can press the back button and redisplay a cached account
  page after the previous user logged out. OWASP tests for exactly this
  ([WSTG-ATHN-06, testing for browser cache weaknesses](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/04-Authentication_Testing/06-Testing_for_Browser_Cache_Weaknesses)).
* **Web cache deception.** An attacker tricks a cache into storing a sensitive
  dynamic page through path tricks like `/account/settings.css`; marking
  dynamic resources `no-store` and `private` is the
  [PortSwigger-documented defense](https://portswigger.net/web-security/web-cache-deception).
* **Web cache poisoning.** The integrity counterpart: an attacker gets a
  harmful response stored and served to other users
  ([PortSwigger on web cache poisoning](https://portswigger.net/web-security/web-cache-poisoning)).
  Strict caching directives shrink what a poisoned cache can hold.

## Risks without it [#risks-without-it]

Without the header, sensitive responses are storable by default. Heuristic
caching lets caches store and reuse responses that carry no caching headers,
and `Set-Cookie` does not inhibit that, so "it sets a session cookie" is no
protection at all. A personalized page served with no directives can sit in
the browser cache of a shared machine and, worse, in a shared cache where it
can answer another user's request.

That is exactly what the
[security headers scanner](/tools/security-headers) means by a
"sensitive response cacheable" finding: a response that sets or requires a
session cookie, with no directive (`no-store` or `private`) preventing
shared-cache storage. The fix is always to send the directive from the server;
no cache will infer it for you.

## Risks and gotchas when using it [#risks-and-gotchas-when-using-it]

* **`no-cache` is not `no-store`.** The classic confusion. `no-cache` means
  the cache may store the response but must revalidate it before reuse. As a
  confidentiality control it does nothing: the bytes are still on disk. When
  you mean do not store, send `no-store`.
* **`private` still lets the browser store.** It only excludes shared caches.
  On a shared machine the page is still in the profile's disk cache, so
  sensitive responses need `no-store`, not `private`.
* **The back/forward cache ignores your directives.** `no-cache` and
  `must-revalidate` have never guaranteed revalidation on a history
  navigation, and since
  [Chrome's 2025 rollout](https://developer.chrome.com/docs/web-platform/bfcache-ccns)
  even `no-store` pages can be restored from the back/forward cache (with
  mitigations: the entry is evicted when cookies change, and it lives at most
  3 minutes). The consequence: logout must actually invalidate the session,
  not just navigate away, and the
  [OWASP Session Management cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)
  recommends sending
  [`Clear-Site-Data`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Clear-Site-Data)
  on logout.
* **The CDN can override you.** CDNs honor the origin's `Cache-Control` by
  default, but edge configuration (for example
  [Cloudflare's cache rules](https://developers.cloudflare.com/cache/concepts/cache-control/))
  can override it, which is how the Steam incident happened. Audit the CDN's
  cache rules along with the header.
* **`Pragma` and `Expires` are legacy.** RFC 9111 deprecates
  [`Pragma`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Pragma)
  (it was historically request-only and was never specified for responses),
  and `Expires` is ignored whenever `max-age` is present. Neither is needed
  for any modern browser.

## How to set it up [#how-to-set-it-up]

1. Classify your responses into three groups: sensitive (anything with
   session or personal data), personalized but safe for the browser to cache,
   and static fingerprinted assets.

2. Send `no-store` on every sensitive response, from the server, per response:

   ```http
   Cache-Control: no-store
   ```

3. Send `private, no-cache` on personalized responses the browser may keep
   but shared caches must not touch:

   ```http
   Cache-Control: private, no-cache
   ```

4. Keep long freshness on static assets with a hash or version in the
   filename; they are the same bytes for every user:

   ```http
   Cache-Control: max-age=31536000, immutable
   ```

5. On logout, invalidate the session server-side and send `Clear-Site-Data`
   so the browser drops what it stored:

   ```http
   Clear-Site-Data: "cache","cookies","storage"
   ```

6. Verify the deployed headers with the
   [security headers scanner](/tools/security-headers), and check that
   no CDN cache rule overrides what the origin sends.

## Recommendation [#recommendation]

Send `Cache-Control: no-store` on every response that carries session or
personal data:

```http
Cache-Control: no-store
```

Send `no-store` on any response containing session identifiers or personal
data, the recommendation in the
[OWASP Session Management cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)
and [WSTG-ATHN-06](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/04-Authentication_Testing/06-Testing_for_Browser_Cache_Weaknesses);
with `no-store` in place, extra directives and `Expires` are generally
unnecessary for modern browsers. Apply it per response, never site-wide:
sensitive pages get `no-store`, personalized pages get `private, no-cache`,
and fingerprinted static assets keep their long `max-age`.

## Browser support [#browser-support]

Baseline widely available: the response directives on this page work in every
current browser, proxy, and CDN, so support is effectively universal. The
thing to verify is not the browser but the edge: a CDN honors your header by
default yet can be configured to override it, so the deployed behavior is only
as good as the cache rules in front of the origin.

## FAQ [#faq]

### What is the difference between no-cache and no-store? [#what-is-the-difference-between-no-cache-and-no-store]

`no-store` tells caches to keep no copy of the response at all. `no-cache`
allows a copy to be stored but requires revalidation with the origin before any
reuse, so the bytes still sit on disk. When you mean do not store a sensitive
response, use `no-store`. See
[no-cache vs no-store](/en/blog/no-cache-vs-no-store) for the full comparison.

### Should login pages be cached? [#should-login-pages-be-cached]

No. Login, account, and any response that carries or requires a session should
send `Cache-Control: no-store`, following the OWASP session management guidance.
Without it a shared cache can store the personalized page and serve it to a
different user, and the back button can redisplay it after logout.

### Does Cache-Control private stop the browser caching? [#does-cache-control-private-stop-the-browser-caching]

No. `private` only tells shared caches, such as CDNs and corporate proxies, not
to store the response; the browser's own cache may still keep it. To prevent
storage everywhere, including on disk in the browser, use `no-store` instead.
`private` narrows the audience, it does not stop caching.

## See also [#see-also]

* [Security headers overview](/en/docs/web-security/security-headers)
* [Cookie security](/en/docs/web-security/security-headers/cookie-security)
  for the session cookies whose responses this header keeps out of caches
* [Security headers scanner](/tools/security-headers) to check your
  caching directives and the rest of your deployed headers

## Sources [#sources]

* [RFC 9111, HTTP caching](https://httpwg.org/specs/rfc9111.html)
* [MDN, Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control)
* [MDN, HTTP caching guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching)
* [OWASP, Session Management cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)
* [OWASP WSTG, testing for browser cache weaknesses](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/04-Authentication_Testing/06-Testing_for_Browser_Cache_Weaknesses)
* [Chrome, back/forward cache with Cache-Control no-store](https://developer.chrome.com/docs/web-platform/bfcache-ccns)
* [SecurityWeek, details of 34,000 Steam users exposed during DDoS attack](https://www.securityweek.com/details-34000-steam-users-exposed-during-ddos-attack/)
