Cache-Control: no-store for Sensitive Pages

This page is part of the Cache-Control and Clear-Site-Data reference and covers one decision precisely: applying Cache-Control: no-store to the routes that render sensitive data — account dashboards, banking views, anything echoing PII or a token — without crippling the cacheability of the rest of the site. no-store is the only directive that keeps a response off disk and out of the back/forward cache, so it is the right tool here; applying it everywhere is a self-inflicted performance outage, so targeting matters as much as the directive itself.

Configuration Syntax & Exact Values

Cache-Control: no-store

For sensitive pages, no-store alone is the correct value. The fuller defensive form, and the alternatives that are not equivalent:

Cache-Control: no-store
Cache-Control: private, no-cache, no-store, must-revalidate
Directive What it does Sufficient for sensitive pages?
no-store No cache (browser, proxy, CDN) may store any part of the response; also makes the document back/forward-cache ineligible. Yes — this is the directive to use
no-cache Response may be stored but must be revalidated with the origin before reuse. Does not prevent on-disk storage or BFCache. No — storage still allowed
private Browser may cache; shared/intermediary caches may not. Stops cross-user leakage in a proxy but leaves the page on the user’s disk and in BFCache. No — fails on shared devices
must-revalidate Forbids serving a stale cached copy after expiry without revalidation. Only as a supplement

The private, no-cache, no-store, must-revalidate combination is belt-and-braces for stubborn intermediaries; the lone no-store already covers all caches per RFC 9111. A legacy note: Pragma: no-cache is an HTTP/1.0 request-header artifact and is not authoritative as a response header in modern browsers — include it only as a hint to ancient proxies, never as a replacement for no-store.

Choosing the directive is a two-question decision: does the response carry per-user or secret data, and does that data need to stay off disk and out of the back/forward cache? The tree below walks those two questions to the correct answer.

Decision tree for choosing a Cache-Control directiveA tree that routes a response to public max-age, no-store, or private based on whether it carries secret data and must stay off disk and BFCache. Per-user or secret data in the response? public, max-age cacheable, CDN-friendly Must stay off disk + out of BFCache? no-store recommended private / no-cache insufficient alone No Yes Yes No
Only a response that carries secret data and must stay off disk and BFCache reaches no-store; everything else stays cacheable.

Server-Side Configuration

Target only sensitive routes. The point of this page is the scoping — a global no-store removes caching and CDN offload from your entire static asset pipeline for no security benefit.

Nginx

# Only authenticated/sensitive routes get no-store
location ~ ^/(account|banking|profile|settings) {
    add_header Cache-Control "no-store" always;
}

# Static assets stay cacheable
location ~* \.(css|js|png|jpg|woff2)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

The always flag emits no-store on 4xx/5xx responses too, so an error rendered on a sensitive route is not accidentally cacheable. Because an add_header in a location replaces inherited headers, the asset block keeps its own caching independently.

Apache

<LocationMatch "^/(account|banking|profile|settings)">
    Header always set Cache-Control "no-store"
</LocationMatch>

<FilesMatch "\.(css|js|png|jpg|woff2)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

always ensures no-store is set even on internally generated error documents, which the default onsuccess table skips. Requires mod_headers.

Cloudflare

Create a Response Header Transform Rule matching URI Path starts with /account (add the other sensitive prefixes), action Set static, header Cache-Control value no-store. Critically, add a Cache Rule on the same path expression set to Bypass cache — otherwise the edge applies its own cache logic and may store the response before honoring origin headers, so the directive alone does not stop edge caching.

Node/Express (Helmet)

// Per-route, not global
const noStore = (req, res, next) => {
  res.set('Cache-Control', 'no-store');
  next();
};

app.get('/account', noStore, accountHandler);
app.get('/banking', noStore, bankingHandler);

Apply the middleware to the sensitive routes only — a blanket app.use(noStore) would strip caching from every response including static assets. Helmet does not set Cache-Control, so this is a plain res.set.

The route-sensitivity map below shows which paths get no-store versus normal caching.

Route sensitivity map for no-store versus cacheable A map splitting routes into a sensitive group that receives no-store and a public group that receives public max-age caching. Sensitive routes Cache-Control: no-store /account, /banking /profile, /settings /logout, /statement Public routes public, max-age, immutable /, /pricing, /docs *.css, *.js, *.woff2 images, fonts no disk, no proxy, no BFCache CDN offload, fast repeat loads
Only authenticated routes receive no-store; public pages and static assets stay cacheable for CDN offload.

Where no-store Takes Effect

no-store is stronger than no-cache or private because it is refused at four distinct storage layers at once, not one. Understanding these layers is what makes the failure modes below diagnosable rather than mysterious.

Storage layers blocked by a no-store responseA single no-store response is refused entry by the browser memory cache, disk cache, shared proxy cache, and back/forward cache. Response with Cache-Control: no-store Browser memory not stored Browser disk not stored Shared / CDN not stored Back/forward ineligible
One no-store response is refused entry by all four storage layers — the reason it defeats the shared-device and Back-button leaks that no-cache and private do not.

Platform note: Chrome and Firefox both treat a no-store main document as BFCache-ineligible and will report the directive as the blocking reason in DevTools. Safari historically restored no-store pages from its page cache for years, so on older WebKit builds pair no-store with a pageshow handler that calls location.reload() when event.persisted is true, and confirm the fix on the actual Safari version your users run.

Diagnostic & Verification Steps

Header presence on a sensitive route:

curl -sI https://example.com/account | grep -i cache-control
# cache-control: no-store

Confirm a static asset is still cacheable (proves the scoping worked):

curl -sI https://example.com/app.css | grep -i cache-control
# cache-control: public, max-age=31536000, immutable

Confirm no shared cache retained the page:

curl -sI https://example.com/account | grep -iE 'cf-cache-status|age|x-cache'
# cf-cache-status: BYPASS

A non-zero Age or a HIT on /account means a shared cache stored an authenticated response — a leak.

BFCache check: open DevTools → Application → Back/forward cache and run the test; a no-store document reports Not eligible with the reason listed as the no-store directive — exactly what you want for an authenticated page. Manually: load /account, navigate away, press Back; the page must reload from the network, not restore from memory.

Edge Cases, Security Implications & Safe Rollback

Frequently Asked Questions

Is no-store or no-cache correct for an account page? no-store. It is the only directive that prevents the response from being stored on disk and keeps it out of the back/forward cache. no-cache still permits storage and only forces revalidation, so a sensitive page can persist locally and be restored by the Back button.

Will no-store hurt performance if I scope it to sensitive routes? Only on those routes, which is acceptable — they should not be cached. Keep static assets and public pages on public, max-age so CDN offload and instant repeat loads are preserved everywhere else. The cost only appears if you apply no-store site-wide.

My CDN still caches the page despite no-store. Why? The edge can apply its own cache logic before honoring origin headers, especially under a forced-cache rule. Add an explicit cache bypass on the sensitive path and confirm the edge reports a bypass/miss rather than a hit.

Conclusion

Roll out incrementally: add no-store to one sensitive route in staging, confirm with curl -sI and the DevTools back/forward cache panel that the document is not cacheable while assets still show public, max-age, then expand the path match to the full sensitive set and promote to production. Keep the match list tight so only authenticated routes pay the caching cost.