no-store and the Browser Back/Forward Cache
Problem Statement
A banking application marks every authenticated page no-store, which is correct for keeping response bodies off disk. It also means pressing the back button triggers a full navigation and a full re-render on every page in the flow, where other sites restore instantly. The directive is doing exactly what it says; the cost is larger than intended because it reaches beyond the HTTP cache.
Prerequisite Concepts
no-cachevsno-store: when to use each — what each directive actually prohibits.- Understanding HTTP cache hierarchy — the browser tier where this behaviour lives.
- When to use no-store for sensitive API responses — the narrower application this page argues for.
Step-by-Step Resolution
Step 1 — Understand what the restore actually is
The back/forward cache is not the HTTP cache. It holds a fully-constructed page — DOM, JavaScript heap, scroll position — in memory so that a back navigation can resume it rather than rebuild it. Because it retains the constructed page rather than a response body, a document marked no-store is disqualified: the browser has been told not to retain this response, and a suspended page is retention.
Step 2 — Measure what it costs
The difference between a restore and a navigation is large enough to be worth quantifying before deciding:
Step 3 — Move the directive onto the data
The sensitive payload is rarely the whole document. Splitting lets each part carry the protection it needs:
GET /account/statements -> Cache-Control: private, no-cache
GET /api/account/statements.json -> Cache-Control: no-store
The document is a shell — layout, navigation, an empty table — that contains nothing worth protecting and is eligible for restore. The payload never touches storage.
Step 4 — Clear or refresh on restore
A restored page reinstates whatever was on screen when the user left, including data that has since been fetched. Handle the restore event explicitly:
window.addEventListener("pageshow", (event) => {
if (!event.persisted) return; // ordinary load, nothing to do
// Restored from the back/forward cache: re-fetch rather than trust the DOM
document.querySelector("#statements").replaceChildren();
refreshStatements();
});
This also solves a correctness problem the HTTP directives never addressed: a restored page can show data that was accurate when the user navigated away and is not any longer.
Step 5 — Verify both properties
The two requirements are independent and both need checking:
# The payload must not be storable
curl -sI https://example.com/api/account/statements.json | grep -i cache-control
# expect: Cache-Control: no-store
# The document must be restore-eligible
curl -sI https://example.com/account/statements | grep -i cache-control
# expect: Cache-Control: private, no-cache
Restore eligibility itself is confirmed in the browser: navigate away, press back, and check whether the pageshow event reports persisted as true. Browser developer tools also report the specific reason a page was disqualified, which is the fastest way to find a blocker that is not the directive.
Step 6 — Decide the trade explicitly rather than by default
Sites reach the all-no-store configuration by accident far more often than by decision. It is the safest-looking option, it passes any review that asks “is sensitive data cached”, and its cost is invisible in every metric except the one nobody measures — how long a back navigation takes.
Making the decision explicit takes one conversation with whoever owns the security requirement. The question to put to them is narrow: does the requirement concern the response bytes reaching persistent storage, or does it concern a fully-rendered page remaining in memory for the duration of the tab’s life? The first is what no-store was designed for and the split preserves it exactly. The second is a different requirement, rarely what a policy actually says, and it does genuinely rule out the restore.
Recording which answer applies, per route, converts an inherited default into a decision that can be revisited. It also tends to shrink the strict list, because most requirements turn out to be about persistence rather than about memory.
Expected Output / Verification
A correctly split page restores instantly on back navigation, re-fetches its data on restore, and leaves nothing sensitive in storage:
back navigation: pageshow persisted=true, 41 ms to interactive
payload request: Cache-Control: no-store, not present in disk cache
document: Cache-Control: private, no-cache, restored from memory
Edge Cases
- An unload handler blocking the restore. A legacy
unloadlistener disqualifies the page regardless of the directives. Usepagehideinstead, which is compatible with the restore. - Open connections. An unclosed WebSocket or an in-flight request can prevent suspension. Close or abort them in
pagehideand re-establish inpageshow. - Session expiry during suspension. A page restored an hour later may hold a session that has since expired. Re-validating on restore is what turns this from a confusing error into a clean redirect.
- Third-party scripts blocking eligibility. An embedded widget holding a connection can disqualify pages the application itself made eligible, and the reason will not be visible in your own code.
- Assuming restore implies staleness is safe. The restore reinstates the DOM exactly as it was. Anything time-sensitive on screen is now as old as the suspension, which the HTTP directives have no bearing on.
- Regulatory requirements naming the document. Where a rule explicitly requires that no part of an authenticated page be retained, the split is not available and the navigation cost has to be accepted.
Frequently Asked Questions
Does no-store on a document really disable the back/forward cache?
Yes. Browsers treat a no-store document as ineligible for the whole-page restore, because the restore reinstates a fully-constructed page from memory — exactly the retention the directive asks them to avoid.
Is the back/forward cache worth protecting?
On navigation-heavy sites it is one of the largest available wins, because a restore is effectively instantaneous compared with a full navigation. Whether it outweighs the data-protection requirement is a per-route decision, not a site-wide one.
Can I keep the restore and still protect the data?
Usually. Put no-store on the API responses that carry the sensitive payload rather than on the surrounding document, and clear or re-fetch that data when the page is restored.
What else disqualifies a page from restore?
An open connection the browser cannot suspend — an unclosed WebSocket or an in-flight fetch — an unload handler, and certain permission or media states. Browsers expose the reason in their developer tools, which is the quickest way to find out.
Related
- When to use no-store for sensitive API responses covers the responses this technique moves the directive onto.
- When does a browser invalidate a cached resource? covers the other browser-side lifecycle events that discard state.
- Public vs private cache scope is the weaker protection that keeps the restore available.
Back to no-cache vs no-store