Cache-Control no-store and the Back/Forward Cache
This guide resolves a common conflict within Back/Forward Cache (bfcache), part of Advanced Caching Strategies & CDN Architecture. Many applications send Cache-Control: no-store on every HTML response — sometimes deliberately for security, often because a framework, middleware or CDN rule sets it globally "to be safe". Browsers have historically treated no-store documents as ineligible for bfcache, so every back navigation to those pages becomes a full reload.
The fix is not to remove no-store everywhere. It is to understand what no-store actually protects against, apply it only where that protection matters, and use no-cache (revalidate before reuse) or private (do not store in shared caches) where the real goal is freshness or keeping data out of CDNs. Chromium has also been experimenting with allowing some no-store pages into bfcache under strict conditions, but that behaviour is not something to design around.
Rapid Diagnosis
- Check HTML response headers on key templates (Network panel → document → Headers). Note
no-storeoccurrences. - Find where it is set. Framework defaults, authentication middleware, CDN page rules and reverse proxies are common sources; one global rule often applies to public pages too.
- Correlate with bfcache field data.
notRestoredReasonsincludes a reason forno-storemain resources; rank templates by how often it appears. - Classify content. Which templates show data that must never be kept on disk (banking, health records)? Which are just "logged-in" or "dynamic"?
Root Cause Analysis
1. Global no-store as a default. Security checklists recommend no-store for sensitive pages; teams apply it to all HTML for simplicity.
2. Confusing no-store with no-cache. no-cache means "revalidate before use" — exactly what most teams want for HTML — but no-store is chosen because the name sounds stronger.
3. CDN safety. no-store is used to stop CDNs caching personalised pages; private achieves that without forbidding browser storage.
4. Logout concerns. Teams fear a user pressing Back after logout and seeing account data; this is a real concern but solvable without no-store on every page.
Step-by-Step Resolution
1. Classify templates by sensitivity
Make an explicit list: public templates, personalised-but-ordinary templates (account dashboard, cart), and sensitive templates (payment details, medical records). Only the last group needs no-store.
2. Set headers per class
# Public HTML: cacheable at the edge, revalidated by browsers.
location / { add_header Cache-Control "public, max-age=0, s-maxage=300, must-revalidate"; }
# Logged-in pages: browser may keep them (bfcache-eligible), CDN must not.
location /account/ { add_header Cache-Control "private, no-cache"; }
# Sensitive pages: never stored.
location /account/payment-methods/ { add_header Cache-Control "no-store"; }
# trade-off: per-path rules need maintenance as routes change. Prefer setting
# headers in the application per route type, with a safe default (private,
# no-cache) for anything authenticated that is not explicitly classified.
Expected outcome: most templates become bfcache-eligible; sensitive ones stay protected.
3. Handle logout and session expiry on restore
If a page is restored from bfcache after the session ended, check on pageshow and redirect.
addEventListener('pageshow', async (event) => {
if (!event.persisted) return;
const res = await fetch('/api/session', { cache: 'no-store', credentials: 'same-origin' });
if (res.status === 401) location.replace('/login?expired=1');
});
// trade-off: the restored page is briefly visible before the redirect. For
// templates where even a moment's display is unacceptable, keep no-store.
Expected outcome: logged-out users never interact with stale authenticated pages.
4. Verify CDN behaviour
Confirm the CDN respects private (does not cache) and that no edge rule rewrites headers back to no-store.
Verification
Check headers on each template class with curl -sI. Run the DevTools bfcache test on public and account pages: they should restore; sensitive pages should not. In RUM, the share of misses attributed to no-store should drop to the sensitive templates only. Have your security team review the classification.
Worked Example: A SaaS Dashboard
A SaaS application's framework sent Cache-Control: no-store on every server-rendered response. Users frequently moved between a project list and project pages with Back; every return reloaded the list, taking 1.8s on average on laptops with slow office Wi-Fi. After classifying templates, the team set private, no-cache on the list and project pages and kept no-store only on billing and API-key pages, and added the session check on pageshow. Back navigations to the project list became instant for 78% of navigations in Chromium, and server load from list requests fell by roughly a quarter.
Common Mistakes
- Removing no-store from everything. Sensitive pages still need it; classify first.
- Using
no-cacheand expecting it to stop CDN caching. Pair it withprivatefor personalised pages. - Forgetting the session check. Without it, a restored page after logout may still show data until the user interacts.
- Assuming
max-age=0equalsno-store. It allows storage and requires revalidation; it is often what teams actually want.
Edge Cases
Pages with CSRF tokens. Restored pages carry the token they were rendered with; if tokens rotate per page view, a form submitted from a restored page may fail. Use per-session tokens or refresh the token on pageshow.
Shared devices. On kiosks or shared computers, policy may require no-store more broadly; that is a legitimate business decision with a performance cost.
Embedded third-party content. A cross-origin iframe served with no-store can also affect eligibility; check frame-level reasons.
Chromium experiments. Chromium has tested restoring no-store pages when no cookies changed; behaviour may differ across versions, so measure rather than assume.
Coordinating With Security Reviews
Header changes on authenticated pages deserve a short written rationale, because no-store often appears in security checklists and auditors look for it. Document, per template class, what the page contains, who can see it, what the realistic threat is (shared devices, back-button exposure after logout), and which control addresses it — no-store, a session check on restore, or short session lifetimes. Most security teams accept private, no-cache plus a restore-time session check for ordinary account pages once the trade-off is explained in those terms, and keep no-store for the handful of pages where any on-disk storage is unacceptable.
FAQ
Does no-cache mean the page will not be cached?
No — it means the page may be stored but must be revalidated with the server before each reuse. It is the right default for HTML that must always be current, and it does not block bfcache.
Is bfcache restoration a security risk for authenticated pages?
The page is restored in the same tab, for the same user who saw it moments before, from memory. The main risk is logout or session expiry on shared devices; the pageshow session check addresses it. Highly sensitive pages can keep no-store.
What does private actually prevent?
It tells shared caches (CDNs, proxies) not to store the response; the user's own browser may still cache it. That is usually exactly what personalised pages need.
Should API responses use no-store?
API caching is a separate decision covered in caching API responses at the CDN. API headers do not affect the document's bfcache eligibility, though the data they return may need refreshing on restore.
Can I test the effect of a header change before deploying?
Yes: DevTools' network request overrides let you change response headers locally, then run the bfcache test. That confirms no-store was the only blocker before you change server configuration.
Related
- no-cache vs no-store vs max-age=0 — the directives in depth.
- Cache-Control for HTML documents — choosing HTML policies generally.
- Caching HTML for logged-in users safely — the edge side of personalised pages.