Your Back Button Isn’t Stale Because of staleTimes — It’s bfcache, and Here’s How to Tune Both

You ship a product listing page. A user opens a detail page, changes a quantity, hits Back, and sees the old quantity. The first instinct is to open next.config.ts and lower experimental.staleTimes.dynamic. That instinct is misdirected, and the Next.js docs say so directly: staleTimes “doesn’t change back/forward caching behavior to prevent layout shift and to prevent losing the browser scroll position” (Next.js staleTimes reference). No value of staleTimes.dynamic or staleTimes.static can fix a back-button freshness problem, because back/forward is not the path that flag governs.

This is a category error worth naming precisely, because it wastes engineering time in a predictable way: you tune a knob, the back button stays stale, you tune it harder, and you’ve now degraded forward-navigation prefetch reuse for a problem that was never in that layer. The fix is to separate the two caches that both affect the back button, diagnose which one served the page, and tune each with the mechanism it actually responds to.

Two caches, two failure modes

A back navigation in a Next.js App Router app can be served by one of two independent caches, and they fail differently.

The Next.js Client Router Cache is a Next.js-level cache of page segments. staleTimes configures how long those segments stay reusable. It is documented as experimental: “This feature is currently experimental and subject to change, it’s not recommended for production” (staleTimes reference).

The browser’s back/forward cache (bfcache) is a different animal entirely. Per web.dev, “bfcache is a snapshot of the entire page in memory, including the JavaScript heap, whereas the HTTP cache contains only the responses for previously made requests” (web.dev bfcache). When a back navigation is served from bfcache, the browser restores the whole page from memory. No request goes out. The Network panel shows nothing. No server-side revalidation setting is consulted, because there is no server round trip to revalidate.

That distinction is the whole diagnosis. If your back navigation produces no Network panel entry, the page came from bfcache and staleTimes was never in play. If it produces a request, the Client Router Cache and the server cache are the relevant layers. You cannot tune the right thing until you know which layer served the page.

What staleTimes actually governs

The flag has two properties, and the docs are specific about when each applies.

dynamic “is used when the page is neither statically generated nor fully prefetched (e.g. with prefetch={true}). Default: 0 seconds (not cached)” (staleTimes reference).

static “is used for statically generated pages, or when the prefetch prop on Link is set to true, or when calling router.prefetch. Default: 5 minutes” (staleTimes reference).

The version history matters here, because advice written against 14.x defaults describes a different system. The dynamic default “changed from 30s to 0s” in v15.0.0 (staleTimes reference). The Next.js 15 release post confirms the intent: “we are changing the default behavior to have a staleTime of 0 for Page segments” (Next.js 15 release).

So staleTimes is a forward-navigation prefetch reuse control. It decides how long a page segment fetched ahead of time stays reusable when the user navigates forward to it. That is a real and useful lever. It is not a back-button freshness lever.

The carve-outs that survive the v15 default change

Even with dynamic at 0, several behaviors remain cached by design, and they explain a lot of “why is this still old” reports.

Shared layout data is not refetched: “Shared layout data won’t be refetched from the server to continue to support partial rendering” (Next.js 15 release). The staleTimes reference puts it the same way: “This doesn’t affect partial rendering, meaning shared layouts won’t automatically be refetched on every navigation, only the page segment that changes” (staleTimes reference).

Back/forward still restores from cache: “Back/forward navigation will still restore from cache to ensure the browser can restore scroll position” (Next.js 15 release).

And loading UI stays cached: “loading.js will remain cached for 5 minutes (or the value of the staleTimes.static configuration)” (Next.js 15 release). The staleTimes reference generalizes this: “Loading boundaries are considered reusable for the static period defined in this configuration” (staleTimes reference).

None of these are bugs. They are the documented shape of the cache.

Why back/forward is deliberately excluded

The exclusion is a tradeoff, not a gap. The docs tie it to two user-visible properties: preventing layout shift and preserving scroll position (staleTimes reference).

Think about what a forced refetch on back navigation would cost. The user hits Back expecting to return to where they were — same scroll offset, same layout, same visual state. If you force a server round trip and a re-render, you buy freshness with exactly the layout shift and scroll loss the default was designed to avoid. That tradeoff should be made explicitly, per route, by an engineer who has decided the data on that route must be current. It should not be made globally by lowering a flag that was never wired to that path.

The correct split of responsibilities

Use each mechanism for what it governs.

Forward-navigation prefetch reuse: staleTimes. If your app prefetches routes and you want those prefetched segments to stay reusable longer, this is the control. Treat it as a reversible experiment, not a load-bearing production configuration, because the docs state the feature is experimental and not recommended for production (staleTimes reference).

Back/forward freshness: a pageshow handler that checks event.persisted. Per web.dev, “The pageshow event fires right after the load event when the page is initially loading and any time the page is restored from bfcache. The pageshow event has a persisted property, which is true if the page was restored from bfcache and false otherwise” (web.dev bfcache).

The pattern is small:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // The page was restored from bfcache.
    // Refresh only the data that must be current.
  }
});

Two constraints on that handler are easy to miss. First, HTTP cache directives do not help you here: “when a page is restored from bfcache, it is restored from memory, not from the HTTP cache. As a result, directives like Cache-Control: no-cache or Cache-Control: max-age=0 are not taken into account, and no revalidation occurs before the content is displayed to the user” (web.dev bfcache). Second, web.dev notes that bfcache “doesn’t work for ‘soft navigations’ within a single-page app” (web.dev bfcache), so the handler covers browser-managed back/forward, not in-app client transitions.

If you need to force a server refetch of the current route from client code, router.refresh() does that — but note its boundary: “This clears the Client Cache for the current route, but does not invalidate the server-side cache” (useRouter reference). Server-side invalidation is a separate concern, handled by revalidatePath or revalidateTag (useRouter reference).

Audit bfcache eligibility before you write the handler

A freshness strategy built on pageshow is silently defeated if the page never enters bfcache. Three constraints are worth auditing first.

Never use unload. “On desktop, Chrome and Firefox have chosen to make pages ineligible for bfcache if they add an unload listener, which is less risky but also disqualifies a lot of pages” (web.dev bfcache). This includes listeners added by third-party scripts, which is why the audit has to cover everything on the page, not just your code.

Add beforeunload only when needed. web.dev recommends you “only add beforeunload listeners when a user has unsaved changes and then remove them immediately after the unsaved changes are saved” (web.dev bfcache). An unconditional listener is the common mistake.

Reserve Cache-Control: no-store for sensitive pages. “Since Cache-Control: no-store restricts a page’s eligibility for bfcache, it should only be set on pages that contain sensitive information where caching of any sort is never appropriate” (web.dev bfcache). Note the source also flags that this behavior is under revision in Chrome, so treat it as a current constraint rather than a permanent law.

If any of these apply to the route you’re debugging, fix them before tuning anything else. A page that never enters bfcache never reaches your pageshow handler.

Measure before you configure

The cheapest diagnostic is also the most decisive. Open the Network panel, perform a back navigation, and look at whether a request was issued.

  • No request: the page came from bfcache. staleTimes was never consulted. Your lever is the pageshow handler, subject to the eligibility constraints above.
  • A request: the Client Router Cache and server cache are the relevant layers. Now staleTimes, router.refresh(), and server-side revalidation are in scope.

To get a hit rate rather than a single observation, instrument the same event you’d use to fix the problem:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // count a bfcache restore
  } else {
    // count a normal load
  }
});

That ratio tells you which layer you are actually debugging across your traffic, instead of which layer you assumed.

A worked example, labeled as hypothetical

Suppose a product listing page where the user opens a detail page, edits a quantity, and hits Back. The listing shows the pre-edit quantity.

If the Network panel shows no request on that Back, the listing was restored from bfcache. Lowering staleTimes.dynamic changes nothing, because the flag does not govern that path. The fix is a pageshow handler that detects event.persisted and refreshes the quantity data — and only that data, so you don’t reintroduce the layout shift and scroll loss the default avoids.

If the Network panel shows a request, the listing was served through the Client Router Cache or the server. Now staleTimes and server-side revalidation are the right conversation, and the pageshow handler is beside the point.

This example is illustrative, not a case record. The diagnostic step — check the Network panel first — is what makes it useful.

One more lever: prefetch invalidation

If you do tune forward-navigation prefetching, router.prefetch accepts an onInvalidate callback that “is called when the prefetched data becomes stale” and “at most once per prefetch request” (useRouter reference). That gives you a hook to re-prefetch when a prefetched segment ages out, rather than guessing at a global staleTimes value. It was introduced in v15.4.0 (useRouter reference).

There is also router.bfcacheId, an opaque identifier that “stays the same for back/forward navigations, router.refresh(), and search-param- or hash-only navigations” (useRouter reference). Its documented use is keying a component to reset state on fresh navigations while preserving it across back/forward — but the docs recommend preferring explicit state resets or data-derived keys, and using bfcacheId “only as a last resort, like when migrating an existing codebase” (useRouter reference).

FAQ

Does staleTimes affect the back button at all?
No. The docs state it “doesn’t change back/forward caching behavior” (staleTimes reference). Back/forward freshness is a bfcache concern.

Why is my back button stale even with staleTimes.dynamic at 0?
Because the page was likely restored from bfcache, which is a full in-memory snapshot and does not consult staleTimes (web.dev bfcache). Check the Network panel for a request on the back navigation.

Will Cache-Control: no-cache force fresh data on back?
No. On a bfcache restore, “directives like Cache-Control: no-cache or Cache-Control: max-age=0 are not taken into account, and no revalidation occurs before the content is displayed to the user” (web.dev bfcache).

Is staleTimes safe to use in production?
The docs say it is “currently experimental and subject to change, it’s not recommended for production” (staleTimes reference). Treat any tuning as a reversible experiment.

Does router.refresh() fix stale server data?
It clears the Client Cache for the current route, but “does not invalidate the server-side cache” (useRouter reference). Use revalidatePath or revalidateTag for server-side invalidation.

What if my app is a single-page app?
bfcache “doesn’t work for ‘soft navigations’ within a single-page app” (web.dev bfcache), so the pageshow approach covers browser-managed back/forward, not in-app client transitions.

The short version

Back-button staleness and forward-navigation prefetch reuse are different problems with different controls. staleTimes governs the latter and is documented not to touch the former. Back/forward freshness belongs to a pageshow handler that checks event.persisted, and that handler only runs if the page is eligible for bfcache — which means auditing unload, beforeunload, and Cache-Control: no-store first. Diagnose with the Network panel before you touch a config file. If no request fires on Back, no staleTimes value was ever in play.