Skip to content

Segment cache: a stalled RSC request permanently kills soft navigation to a route (zero-request dropped clicks with staleTimes.dynamic > 0) #98066

Description

@alireza-attari

Link to the code that reproduces this issue

https://github.com/alireza-attari/next-staletimes-bfcache-repro

To Reproduce

  1. npm install && npm run build && npm start (config sets experimental.staleTimes: { dynamic: 60, static: 300 })
  2. Open http://localhost:3000 and press Run reproduction (fully automated, ~80s; it logs every step with RSC request counts)

The harness shims window.fetch so RSC requests for /target (a force-dynamic page whose link is on screen, so its route tree is already prefetched) hang, clicks the link once while that request is in flight, restores fetch, then keeps clicking /target and logs whether the URL changed and how many RSC requests each click issued.

Run it a second time with npm run build:fixed && npm start (same app, staleTimes.dynamic unset) to compare.

Current vs. Expected behavior

Current, build with staleTimes.dynamic: 60 (verbatim from the harness log):

[t+3.0s]   poisoning click: DEAD — URL unchanged after 3s, 1 RSC request(s) issued
[t+3.0s] step 3 — fetch shim REMOVED, network fully restored
[t+7.0s]   click ~1s after restore: DEAD — URL unchanged after 3s, 0 RSC request(s) issued
[t+15.0s]  click ~5s after restore: DEAD — URL unchanged after 3s, 0 RSC request(s) issued
[t+28.0s]  click ~10s after restore: DEAD — URL unchanged after 3s, 0 RSC request(s) issued
[t+78.1s]  click ~65s after the poisoning click: DEAD — URL unchanged after 5s, 1 RSC request(s) issued

Current, same scenario with staleTimes.dynamic unset: every post-restore click issues a fresh RSC request, and the navigation still never commits.

In both builds /target stays dead until a full page reload; nothing is logged, no error surfaces, and the /other control link keeps navigating normally in the same session — the poisoning is per-route.

Expected: a navigation whose payload fetch stalled or failed falls back to a full-document navigation, as the pre-segment-cache router did (it logged "Failed to fetch RSC payload … Falling back to browser navigation"). A later click with a healthy network should never be silently dropped, and certainly not dropped without issuing any request.

From reading the compiled dist in 16.3.3, three problems compound:

  1. A never-settling promise is stored in the BFCache. writeToBFCache (client/components/segment-cache/ppr-navigations.js) runs during tree construction, before any response exists, so a navigation that adopted an in-flight prefetch entry (the Pending branch of readSegmentCacheEntryForNavigation) parks an unsettled rsc/prefetchRsc promise there.
  2. With staleTimes.dynamic > 0 that poisoned entry is then served: readFromBFCacheDuringRegularNavigation (case FreshnessPolicy.Default) returns it with needsDynamicRequest: false, so the dropped clicks issue zero requests until staleAt = now + dynamic * 1000 expires — the failure is invisible in the network tab for exactly dynamic seconds. With the default 0 the entry is always expired and this branch never serves.
  3. Even when a fresh request is issued, React stays parked on the stale prefetch thenable: layout-router.js renders useDeferredValue(cacheNode.rsc, resolvedPrefetchRsc), which suspends on prefetchRsc first, and a later dynamic response resolving cacheNode.rsc does not re-wake the transition. The failure is silent because navigateToUnknownRoute(...).catch(() => state) (segment-cache/navigation.js) swallows it.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.6.0
  Available memory (MB): 18432
  Available CPU cores: 11
Binaries:
  Node: 22.23.1
  npm: 10.9.8
  Yarn: 1.22.22
  pnpm: 11.22.0
Relevant Packages:
  next: 16.3.3 // Latest available version is detected (16.3.3).
  eslint-config-next: N/A
  react: 19.2.8
  react-dom: 19.2.8
  typescript: 7.0.2
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Navigation

Which stage(s) are affected? (Select all that apply)

next start (local), Other (deployed production)

Additional context

We first hit this as recurring user reports in a production dashboard (~130 static-shell routes, heavy router.prefetch usage), with no fetch shims and no network fault involved: intermittent silently-dead sidebar clicks — zero in-flight requests, idle main thread, URL frozen — deterministic per route for a window, self-healing after exactly staleTimes.dynamic seconds. The heal window tracking the config value (60s) is what led us to the BFCache staleAt. Removing staleTimes.dynamic from the config eliminated the zero-request variant in production (36/36 navigations clean vs 6/24 silently dropped in the same measurement loop before). The fetch shim in the repro only makes the poisoning step deterministic.

Related reports:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions