AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Segment Cache: Sibling PPR Route Prefetches Are Permanently Dropped After Same-Tick Batch

Batching router.prefetch calls for multiple slugs of a PPR route in the same tick causes the first-scheduled sibling to be deferred by the 4-request bandwidth cap. When bandwidth frees, the sibling incorrectly resolves against the fallback route entry populated by other completed prefetches instead of fetching its own per-URL segment. Subsequent prefetches become permanent no-ops, forcing full dynamic navigation for the affected slug.

mediumConfidence 78%Next.jsAffected V16.3.0Affected V16.3.1-Canary.8

Origin Analysis

The segment cache scheduler resolves a deferred per-URL prefetch task against a fallback route entry (segmentPath === null) for the same route key, after that fallback entry was populated by sibling prefetches; this bypasses the specific segment fetch and leaves the cache in a state where later explicit prefetches are suppressed.
1. Create a partially-prerendered route /products/[slug] with cacheComponents: true, partialPrefetching: true, and 5 generateStaticParams slugs. 2. Build and start Next.js 16.3.x. 3. In the browser, call router.prefetch for all 5 sibling URLs in the same tick. 4. Observe that only 4 network requests are issued initially, and no request for the first-scheduled slug (alpha) appears even after bandwidth frees. 5. Wait and call router.prefetch('/products/alpha') again; still zero network. 6. Navigate to /products/alpha; content requires a full dynamic round trip instead of using prefetched segment.

Fixing Code Block

function resolveTask(task) { const cacheEntry = task.cache.get(task.routeKey); if (!cacheEntry) { task.needsFetch = true; return false; } // Critical: a fallback entry (segmentPath === null) must not satisfy a task // waiting on a specific per-URL segment path (segmentPath !== null). // Sibling PPR responses can populate the fallback entry while this task is // deferred for network bandwidth; resolving against it drops the fetch and // makes later prefetches permanent no-ops. if (task.segmentPath !== null && cacheEntry.segmentPath === null) { task.needsFetch = true; return false; } if (cacheEntry.isValid(task)) { task.resolveWithCache(cacheEntry); return true; } task.needsFetch = true; return false; }
The scheduler's resolveTask previously accepted a cache entry as long as the route key matched, ignoring the difference between a per-URL segment (segmentPath non-null) and a fallback entry (segmentPath null). The patch inserts a guard before the normal validity check: if the task requests a specific segment path but the cache entry is fallback, the task is forced to issue a network fetch. This ensures the slug-specific tree/page segment is fetched when bandwidth frees, prevents permanent no-op prefetches, and restores the expected behavior.

Edge Case Audit

This patch assumes any cache entry with segmentPath === null is a fallback entry that must not satisfy a per-URL segment task. If a valid per-URL shell ever has a null segmentPath in a future refactor, the guard would bypass cache and increase prefetch traffic. Rollback should be done by reverting to the previous resolveTask; note that client caches from the unfixed version may continue exhibiting no-op prefetches until a full page reload. Test against dynamic routes without generateStaticParams, streaming fallback states, and concurrent prefetches to avoid introducing duplicate fetches or cache thrashing.

Ecosystem Topology