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.

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.

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.