Link to the code that reproduces this issue
https://github.com/vicanera/next-cached-navigations-cache-handler-race
To Reproduce
- Next 16.3.4,
cacheComponents: true, partialPrefetching: true, and a cacheHandlers.default whose get awaits I/O. In the linked repo slow-cache-handler.js wraps Next's own default in-memory handler and adds a few milliseconds of latency to get at runtime, standing in for a Redis/Memcached handler.
- A page whose cached reads arrive in more than one batch (a second batch of
"use cache" reads starts only after the first batch has resolved), with a cookie-bound child behind <Suspense> so the route is a dynamic render with a static shell.
next build && next start, then request the page repeatedly with NEXT_PRIVATE_DEBUG_CACHE=1.
In a production app with this configuration and a Redis-backed handler, roughly half of the renders of the affected page log three E1163 warnings (one per cached read in the second batch); routes whose reads all start in one batch never do. Fallback-param renders additionally log NEXT_STATIC_GEN_BAILOUT ("Next.js encountered uncached or runtime data during prerendering") from the same spawned prerender.
Honest status of the linked repo: the isolated shape there has not yet fired the warning locally (0/30 at 5 ms and 20 ms handler latency). The debug trail below is from the production app on the same Next version; the exact batching that opens the window has not been reduced further. Included because the source-level mechanism is deterministic and the workaround is verified.
Current vs. Expected behavior
Current. With experimental.cachedNavigations (auto-enabled under cacheComponents) and Partial Prefetching on, every dynamic render (renderToStream for documents, generateStagedDynamicFlightRenderResultNode for RSC navigations) installs a CacheSignal and, on cacheSignal.cacheReady(), spawns spawnRuntimePrefetchWithFilledCaches (mode rewindable-session-shell). cacheReady() is a quiescence heuristic (cache-signal.js: read count 0 → setImmediate → setTimeout(0) with no new beginRead). In use-cache-wrapper.js, a cache-handler hit is written into the resume data cache only after await cacheHandler.get() (the "leader resolved with cache handler hit" path), whereas a generated entry is registered before the function runs (saveToResumeDataCache). So when a render's reads come in two batches, the count touches zero between them; with an async handler the spawned prerender then looks keys up while the second batch's gets are still pending, misses, warns:
Error: Unexpected cache miss after cache warming phase during prerendering. This is likely caused by non-deterministic arguments ... (E1163)
and demotes the segment to a runtime hole. Every miss shows the same ordering for the affected key (NEXT_PRIVATE_DEBUG_CACHE=1):
use-cache: Resume Data Cache entry not found [key] <- the render's own lookup
use-cache: registering as intra-request invocation leader [key]
use-cache: registering as cross-request invocation leader [key]
use-cache: Resume Data Cache entry not found [key] <- the spawned prerender's lookup
Error: Unexpected cache miss after cache warming phase during prerendering ...
use-cache: leader resolved with cache handler hit [key] <- the handler get resolves last
A request that does not miss shows leader resolved with cache handler hit before the second lookup. The arguments are deterministic; the message's diagnosis (non-deterministic arguments) does not apply. With the default in-memory handler get resolves within the same tick, so the race never shows there.
Expected. No warning and no demotion when cache keys are deterministic and the handler simply takes a few milliseconds. Either the pending handler read should hold the CacheSignal until the entry is registered in the resume data cache, or a handler hit should be registered in the resume data cache as a pending entry before the get is awaited, as the generated-entry path already does.
Workaround. experimental: { cachedNavigations: false } → 0 warnings over 85 requests in the production app; prefetch-driven navigations (next-router-prefetch 2/3, which take generateRuntimePrefetchResult) unaffected. The same code shape is present in 16.4.0-canary.26.
Provide environment information
Operating System:
Platform: darwin (production: linux x64 container)
Arch: arm64
Binaries:
Node: 26.2.0
npm: 11.13.0
Relevant Packages:
next: 16.3.4
react: 19.2.0
react-dom: 19.2.0
Next.js Config:
cacheComponents: true, partialPrefetching: true, cacheHandlers.default: custom (async get)
Which area(s) are affected? (Select all that apply)
Cache Components, Partial Prefetching / Runtime Prefetching, Use Cache
Which stage(s) are affected? (Select all that apply)
next start (local), Other (deployed to a self-hosted container)
Additional context
Also reproduced with a real Redis-backed cacheHandlers.default pointed at a throwaway redis:7-alpine: 45 warnings over 30 requests of the affected page on the merged build, 0 over 85 with cachedNavigations: false. Happy to add the debug trail of a failing request in full if useful.
Link to the code that reproduces this issue
https://github.com/vicanera/next-cached-navigations-cache-handler-race
To Reproduce
cacheComponents: true,partialPrefetching: true, and acacheHandlers.defaultwhosegetawaits I/O. In the linked reposlow-cache-handler.jswraps Next's own default in-memory handler and adds a few milliseconds of latency togetat runtime, standing in for a Redis/Memcached handler."use cache"reads starts only after the first batch has resolved), with a cookie-bound child behind<Suspense>so the route is a dynamic render with a static shell.next build && next start, then request the page repeatedly withNEXT_PRIVATE_DEBUG_CACHE=1.In a production app with this configuration and a Redis-backed handler, roughly half of the renders of the affected page log three
E1163warnings (one per cached read in the second batch); routes whose reads all start in one batch never do. Fallback-param renders additionally logNEXT_STATIC_GEN_BAILOUT("Next.js encountered uncached or runtime data during prerendering") from the same spawned prerender.Honest status of the linked repo: the isolated shape there has not yet fired the warning locally (0/30 at 5 ms and 20 ms handler latency). The debug trail below is from the production app on the same Next version; the exact batching that opens the window has not been reduced further. Included because the source-level mechanism is deterministic and the workaround is verified.
Current vs. Expected behavior
Current. With
experimental.cachedNavigations(auto-enabled undercacheComponents) and Partial Prefetching on, every dynamic render (renderToStreamfor documents,generateStagedDynamicFlightRenderResultNodefor RSC navigations) installs aCacheSignaland, oncacheSignal.cacheReady(), spawnsspawnRuntimePrefetchWithFilledCaches(moderewindable-session-shell).cacheReady()is a quiescence heuristic (cache-signal.js: read count 0 →setImmediate→setTimeout(0)with no newbeginRead). Inuse-cache-wrapper.js, a cache-handler hit is written into the resume data cache only afterawait cacheHandler.get()(the "leader resolved with cache handler hit" path), whereas a generated entry is registered before the function runs (saveToResumeDataCache). So when a render's reads come in two batches, the count touches zero between them; with an async handler the spawned prerender then looks keys up while the second batch'sgets are still pending, misses, warns:and demotes the segment to a runtime hole. Every miss shows the same ordering for the affected key (
NEXT_PRIVATE_DEBUG_CACHE=1):A request that does not miss shows
leader resolved with cache handler hitbefore the second lookup. The arguments are deterministic; the message's diagnosis (non-deterministic arguments) does not apply. With the default in-memory handlergetresolves within the same tick, so the race never shows there.Expected. No warning and no demotion when cache keys are deterministic and the handler simply takes a few milliseconds. Either the pending handler read should hold the
CacheSignaluntil the entry is registered in the resume data cache, or a handler hit should be registered in the resume data cache as a pending entry before thegetis awaited, as the generated-entry path already does.Workaround.
experimental: { cachedNavigations: false }→ 0 warnings over 85 requests in the production app; prefetch-driven navigations (next-router-prefetch2/3, which takegenerateRuntimePrefetchResult) unaffected. The same code shape is present in16.4.0-canary.26.Provide environment information
Which area(s) are affected? (Select all that apply)
Cache Components, Partial Prefetching / Runtime Prefetching, Use Cache
Which stage(s) are affected? (Select all that apply)
next start (local), Other (deployed to a self-hosted container)
Additional context
Also reproduced with a real Redis-backed
cacheHandlers.defaultpointed at a throwawayredis:7-alpine: 45 warnings over 30 requests of the affected page on the merged build, 0 over 85 withcachedNavigations: false. Happy to add the debug trail of a failing request in full if useful.