[PR #932] [MERGED] fix(cache): serve stale unstable_cache entries in App Router #960

Closed
opened 2026-05-06 13:11:15 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/932
Author: @NathanDrake2406
Created: 4/28/2026
Status: Merged
Merged: 4/28/2026
Merged by: @james-elicx

Base: mainHead: nathan/unstable-cache-swr


📝 Commits (1)

  • fa2f7d7 fix(cache): serve stale unstable_cache entries

📊 Changes

5 files changed (+273 additions, -33 deletions)

View changed files

📝 packages/vinext/src/entries/app-rsc-entry.ts (+3 -0)
📝 packages/vinext/src/shims/cache.ts (+112 -33)
📝 packages/vinext/src/shims/unified-request-context.ts (+1 -0)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+18 -0)
📝 tests/shims.test.ts (+139 -0)

📄 Description

Summary

  • Make unstable_cache distinguish normal App Router requests from cache-regeneration scopes.
  • Return stale FETCH entries immediately for normal App Router requests, while scheduling a deduped background refresh through waitUntil when available.
  • Keep stale unstable_cache entries blocking in route/page revalidation scopes so regenerated ISR artifacts are written with fresh data.

Why

CacheHandler.get() already preserves time-expired FETCH entries as cacheState: "stale", but unstable_cache treated stale entries as a miss and awaited the callback before responding. That put callback latency on every TTL boundary for App Router requests instead of serving stale data while refreshing in the background.

This is a meaningful parity/performance fix rather than speculative cleanup: any unstable_cache(..., { revalidate }) entry with a slow callback added foreground latency each time its TTL elapsed.

Next.js parity references

Implementation notes

  • UnifiedRequestContext now carries an unstableCacheRevalidation mode.
  • Normal App Router request entry sets that mode to background.
  • Generated App Router revalidation contexts set it to foreground.
  • The default remains foreground, preserving Pages Router and outside-render behavior.
  • Background refreshes are deduped by cache key and always settle/clean up their pending map entry.

Validation

  • vp test run tests/shims.test.ts -t "unstable_cache serves stale entries" failed before the fix with stale requests blocking on regeneration.
  • vp test run tests/shims.test.ts -t "unstable_cache (serves stale entries|blocks on stale entries|re-fetches when entry is stale)"
  • vp check tests/shims.test.ts
  • vp test run tests/entry-templates.test.ts
  • vp test run tests/unified-request-context.test.ts
  • vp test run tests/shims.test.ts
  • vp test run tests/app-router.test.ts -t "route handler ISR: STALE serves stale data"
  • vp check
  • vp run vinext#build

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/932 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 4/28/2026 **Status:** ✅ Merged **Merged:** 4/28/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `nathan/unstable-cache-swr` --- ### 📝 Commits (1) - [`fa2f7d7`](https://github.com/cloudflare/vinext/commit/fa2f7d772c3ba5114447a486952bed5103150134) fix(cache): serve stale unstable_cache entries ### 📊 Changes **5 files changed** (+273 additions, -33 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+3 -0) 📝 `packages/vinext/src/shims/cache.ts` (+112 -33) 📝 `packages/vinext/src/shims/unified-request-context.ts` (+1 -0) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+18 -0) 📝 `tests/shims.test.ts` (+139 -0) </details> ### 📄 Description ## Summary - Make `unstable_cache` distinguish normal App Router requests from cache-regeneration scopes. - Return stale `FETCH` entries immediately for normal App Router requests, while scheduling a deduped background refresh through `waitUntil` when available. - Keep stale `unstable_cache` entries blocking in route/page revalidation scopes so regenerated ISR artifacts are written with fresh data. ## Why `CacheHandler.get()` already preserves time-expired `FETCH` entries as `cacheState: "stale"`, but `unstable_cache` treated stale entries as a miss and awaited the callback before responding. That put callback latency on every TTL boundary for App Router requests instead of serving stale data while refreshing in the background. This is a meaningful parity/performance fix rather than speculative cleanup: any `unstable_cache(..., { revalidate })` entry with a slow callback added foreground latency each time its TTL elapsed. ## Next.js parity references - Next.js App Router `unstable_cache` schedules `pendingRevalidates` for stale entries, awaits it only during static generation, and otherwise returns the cached response immediately: https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/server/web/spec-extension/unstable-cache.ts#L246-L298 - Next.js no-work-store path, used for Pages/outside-render calls, only returns fresh entries and awaits a new result for stale/miss cases: https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/server/web/spec-extension/unstable-cache.ts#L330-L387 - Next.js has a production regression test requiring foreground `unstable_cache` revalidation while an ISR page is regenerating: https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/test/production/app-dir/unstable-cache-foreground-revalidate/unstable-cache-foreground-revalidate.test.ts#L13-L72 - The incremental cache marks `FETCH` entries stale by age/tags rather than deleting them, which is what makes stale-while-revalidate possible: https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/server/lib/incremental-cache/index.ts#L546-L565 ## Implementation notes - `UnifiedRequestContext` now carries an `unstableCacheRevalidation` mode. - Normal App Router request entry sets that mode to `background`. - Generated App Router revalidation contexts set it to `foreground`. - The default remains `foreground`, preserving Pages Router and outside-render behavior. - Background refreshes are deduped by cache key and always settle/clean up their pending map entry. ## Validation - `vp test run tests/shims.test.ts -t "unstable_cache serves stale entries"` failed before the fix with stale requests blocking on regeneration. - `vp test run tests/shims.test.ts -t "unstable_cache (serves stale entries|blocks on stale entries|re-fetches when entry is stale)"` - `vp check tests/shims.test.ts` - `vp test run tests/entry-templates.test.ts` - `vp test run tests/unified-request-context.test.ts` - `vp test run tests/shims.test.ts` - `vp test run tests/app-router.test.ts -t "route handler ISR: STALE serves stale data"` - `vp check` - `vp run vinext#build` --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:11:15 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#960
No description provided.