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.
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
- Performance cost.
no-storedisables all caching for the response: no conditional revalidation, no CDN offload, no instant Back navigation. That is acceptable and intended for sensitive routes, but every route it covers pays full round-trip and render cost on each view. Keep the match list tight. - Applied site-wide by mistake. A
no-storeat the server root or a global middleware strips caching from CSS, JS, fonts, and public pages — a large, silent performance regression. Symptom: every asset re-downloads on each navigation and CDN cache-hit ratio collapses. Fix: move the directive into per-pathlocation/LocationMatch/route middleware and confirm assets showpublic, max-ageagain. Rollback is the same edit — remove the global directive and re-scope. - CDN or proxy caches it anyway. An intermediary that applies its own cache logic, or a forced-cache rule, can store the response before honoring
no-store. Add an explicit bypass: Cloudflare Cache Rules Bypass cache on the path, Nginxproxy_no_cache 1; proxy_cache_bypass 1;, ApacheCacheDisable /account. Verifycf-cache-status: BYPASS. - Back button still shows the page. If a sensitive page reappears via the Back button after logout, the response used
no-cacheorprivate, notno-store— onlyno-storemakes the document BFCache-ineligible. Switch the directive. Pair with Clear-Site-Data on logout so cookies and storage are purged at the same time, and with HSTS so the page is never served over plaintext.
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.