When to Use no-store for Sensitive API Responses

An API endpoint that returns an authentication token, a full profile with personally identifiable information, or an account balance cannot rely on no-cache or private alone — both still permit the response body to be written to disk or memory somewhere in the request chain. When the data itself is the risk, not just its freshness, no-store is the only directive that removes storage as a possibility.

Prerequisite Concepts

Before configuring sensitive endpoints, be comfortable with:

Deciding whether a response needs no-storeno-store carries a real performance cost, so it should be applied to the responses that genuinely cannot persist rather than to everything authenticated.Does the body contain a credential or token?yesno-storenoIs it regulated personal or financial data?yesno-storenoIs it user-specific but not sensitive?yesprivate, no-cachenopublic with a short s-maxage
Reserve no-store for what genuinely must not persist

Step-by-Step Resolution

Every tier that must be told not to storeA single missing directive anywhere along the path leaves a copy behind, so the response must be marked at the origin and respected at every hop.Originemits no-storeCache-ControlShieldnothing writtenhonouredEdge PoPnothing writtenhonouredBrowsernothing on disk
One directive, honoured at four separate storage points

Step 1 — Identify which responses actually require no-store

Not every authenticated response needs no-store. Reserve it for responses whose body content itself is sensitive if persisted anywhere, including the requesting user’s own device:

  • Authentication tokens, session identifiers, one-time codes, and password-reset links.
  • Full personally identifiable information (PII) — government IDs, dates of birth, full addresses.
  • Financial data — account balances, transaction histories, card details.
  • Any response served over an assumption of “seen once, then gone,” such as a document requiring destruction after viewing.

Responses that are merely user-specific but not independently dangerous if cached to disk (a user’s public display name, a non-sensitive preferences object) are usually adequately served by private with a short max-age, without paying the full cost of no-store on every request.

Step 2 — Set the full header combination on the response

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store, private
Pragma: no-cache
Vary: Authorization

Each piece has a distinct job:

  • no-store — the primary directive. RFC 9111 §5.2.2.5 mandates that no cache may store any part of this response, including the headers, at any tier: browser memory, browser disk, CDN edge, reverse proxy, or corporate gateway.
  • private — belt-and-suspenders scope restriction. Some older or misconfigured intermediaries that do not fully respect no-store may still respect private and refuse to share the response across users; it costs nothing to include both.
  • Pragma: no-cache — a legacy HTTP/1.0 fallback. Ancient proxies that predate Cache-Control entirely may still check Pragma. On any RFC 9111-compliant cache it is redundant with no-store, but it is free insurance for older infrastructure you don’t control.
  • Vary: Authorization — tells any cache that does exist in front of the origin (even one violating no-store, or caching only the response for retry logic) that responses must never be shared across different Authorization header values. This is a defense-in-depth measure, not a substitute for no-store.

Step 3 — Verify no storage occurs at any tier

curl — confirm no Age header ever appears:

curl -sI https://api.example.com/account/balance \
  -H "Authorization: Bearer $TOKEN" \
  | grep -iE '(cache-control|pragma|vary|age|cf-cache-status)'

Expected output:

HTTP/1.1 200 OK
Cache-Control: no-store, private
Pragma: no-cache
Vary: Authorization
CF-Cache-Status: DYNAMIC

CF-Cache-Status: DYNAMIC (Cloudflare) confirms the edge never attempted to store the response. No Age header should ever appear on any repeated request — its presence would mean a cache tier is retaining the response in violation of no-store.

Repeat with a second token to confirm no cross-user bleed:

curl -sI https://api.example.com/account/balance -H "Authorization: Bearer $TOKEN_A" | grep -i cf-cache-status
curl -sI https://api.example.com/account/balance -H "Authorization: Bearer $TOKEN_B" | grep -i cf-cache-status

Both requests must independently show DYNAMIC (or an equivalent “not served from cache” signal). If either shows HIT, a shared cache in the path is storing and reusing a response across distinct users — an active data leak that must be fixed immediately, either by correcting no-store handling upstream or by confirming Vary: Authorization is actually being honored by that layer.

Step 4 — Check back/forward cache exclusion in the browser

For a full HTML page (not just a JSON API response) carrying sensitive data, confirm it is excluded from the browser’s back/forward cache (bfcache):

  1. Open Chrome DevTools → Application tab → Back/forward cache section.
  2. Navigate to the sensitive page, then away to another page.
  3. Click the browser’s Back button, then re-run the bfcache diagnostic in DevTools.
  4. Confirm the report shows the page was not restored from bfcache, with a reason similar to “the response had a Cache-Control: no-store header.”
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: no-store, private

If DevTools instead shows the page was served from bfcache, verify that no-store is present on the actual document response the browser navigated to (not just on a subordinate API call made from that page) — bfcache eligibility is evaluated per top-level navigation response.

Expected Output / Verification

  • No Age header appears on any response, on any repeated request, from any cache tier — its presence indicates a storage violation.
  • CF-Cache-Status: DYNAMIC or an equivalent non-cached status appears on every request; HIT at any point is a leak.
  • Two different Authorization tokens against the same endpoint never receive each other’s response bodies.
  • Chrome DevTools’ back/forward cache diagnostic explicitly reports the page as ineligible due to no-store, confirming the browser will not silently restore sensitive state from memory after navigation.
Where a body can survive when no-store is missingFour storage locations that retain a response body, with what remains recoverable after the user believes the session ended.Survives logoutSurvives browser restartReadable byBrowser disk cacheanyone with the profileService worker cacheany script on the originCDN edge storageother users, if publicProxy in the pathnetwork operator
Four places an unprotected response body outlives the session

Edge Cases

  • Shared-cache leakage from proxy misconfiguration — a reverse proxy or corporate gateway that does not fully implement RFC 9111 may cache a response despite no-store if it was configured with a blanket “cache everything” rule at the infrastructure level, overriding origin headers. Audit intermediary configuration directly; header correctness at the origin is necessary but not sufficient.
  • CDN edge functions caching before header inspection — some CDN compute platforms execute request-collapsing or edge logic before the origin’s Cache-Control header is even evaluated, which can transiently buffer a response. Confirm your CDN’s documented behavior for no-store specifically, not just its default cache rules.
  • Missing Vary: Authorization on a technically no-store-compliant cache — if an intermediary is only partially compliant and stores responses for retry or coalescing purposes despite no-store, the absence of Vary: Authorization is what allows that stored copy to be replayed to a different user. Always set both directives together for defense in depth.
  • bfcache and subresources — excluding the top-level document from bfcache via no-store does not automatically clear sensitive data held in JavaScript memory or IndexedDB; those require explicit application-level cleanup on navigation away from the page.

Frequently Asked Questions

Is private enough for sensitive API responses, or do I need no-store?

private only restricts storage to single-user caches, such as the browser cache — it still permits storage. For data that must never persist to disk or memory anywhere, including in the requesting browser, no-store is required. Use private alone only for data that is safe to keep in the user’s own browser cache but not in a shared CDN.

Why add Pragma: no-cache alongside Cache-Control: no-store?

Pragma: no-cache is a legacy HTTP/1.0 directive that some very old caches and proxies still honor when they do not fully implement Cache-Control. It provides a compatibility fallback and costs nothing to include, though on any RFC 9111-compliant cache it is redundant with no-store.

Does no-store prevent the browser back/forward cache from storing the page?

For the main document response, yes in most current browsers — a no-store response is excluded from bfcache, forcing a full reload on back navigation. This is a deliberate trade-off: it prevents a sensitive page from being restored from memory after the user has navigated away, at the cost of losing the instant back-navigation performance benefit.

Can a shared cache leak a no-store response to a different user?

No, if no-store is honored correctly, because nothing is ever stored to serve back to anyone. The leakage risk instead comes from omitting no-store and relying on private or no-cache alone combined with a missing Vary: Authorization, which can let a misconfigured shared cache key requests incorrectly and serve one user’s response to another.


Back to no-cache vs no-store: When to Use Each