Skip to content

Restore dev-mode cache-fill timeout for 'use cache'#93055

Merged
unstubbable merged 3 commits intocanaryfrom
hl/use-cache-dev-timeout
Apr 21, 2026
Merged

Restore dev-mode cache-fill timeout for 'use cache'#93055
unstubbable merged 3 commits intocanaryfrom
hl/use-cache-dev-timeout

Conversation

@unstubbable
Copy link
Copy Markdown
Contributor

When 'use cache' fills during next dev, there was no timeout, so a stalled fill could hang the request indefinitely. This change restores a dev-mode cache-fill timeout that fires 50 seconds after the fill starts, matching the 50s timeout we already have during prerender. The timer sets workStore.invalidDynamicUsageError to a UseCacheTimeoutError and aborts the signal passed to React's renderToReadableStream, which errors the cache stream. From there the existing handling of invalidDynamicUsageError in app-render surfaces the error in the Redbox and the CLI output without any additional plumbing.

Prior to PR #85747, spawnDynamicValidationInDev reused the prerender store and inherited the prerender cache timeout as a side effect. After that change the validation became staged-rendering based and the timeout stopped applying to dev renders. This PR closes that gap.

The timeout is skipped when the render is already in the Dynamic stage, which mirrors prerender, where caches past await connection() aren't executed at all. The check is written as exact equality on RenderStage.Dynamic rather than a numeric < Dynamic comparison because RenderStage.Abandoned is numerically higher than Dynamic but semantically represents an aborted initial prospective render whose pending caches still need to be timed out while the outer flow awaits cacheSignal.cacheReady(). The 50s duration is shared with the prerender path via a new USE_CACHE_FILL_TIMEOUT_MS constant and carries a TODO to derive it from the user-configurable staticPageGenerationTimeout.

The tests model a realistic migration hazard: a module-scoped in-flight fetch dedupe loader (the hand-rolled variant of the documented React.cache-based preload pattern) joined by a 'use cache' function. That configuration deadlocks because the outer fetch is parked on the Dynamic stage in dev, or returns an intentionally hanging promise during prerender, and the cache never fills.

@unstubbable unstubbable marked this pull request as ready for review April 20, 2026 13:59
@unstubbable unstubbable requested a review from gnoff April 20, 2026 13:59
Copy link
Copy Markdown
Contributor Author

unstubbable commented Apr 20, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment thread packages/next/src/server/use-cache/use-cache-wrapper.ts Outdated

await expect(browser).toDisplayRedbox(`
{
"code": "E394",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generic error code. can we create a specific error so that it gets a code automatically?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see whether I can make the next-error-code-swc-plugin handle this special case (error message hard-coded into the constructor of UseCacheTimeoutError) in a follow-up.

Copy link
Copy Markdown
Contributor Author

@unstubbable unstubbable Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When `'use cache'` fills during `next dev`, there was no timeout, so a
stalled fill could hang the request indefinitely. This change restores a
dev-mode cache-fill timeout that fires 50 seconds after the fill starts,
matching the 50s timeout we already have during prerender. The timer
sets `workStore.invalidDynamicUsageError` to a `UseCacheTimeoutError`
and aborts the signal passed to React's `renderToReadableStream`, which
errors the cache stream. From there the existing handling of
`invalidDynamicUsageError` in app-render surfaces the error in the
Redbox and the CLI output without any additional plumbing.

Prior to PR #85747, `spawnDynamicValidationInDev` reused the prerender
store and inherited the prerender cache timeout as a side effect. After
that change the validation became staged-rendering based and the timeout
stopped applying to dev renders. This PR closes that gap.

The timeout is skipped when the render is already in the Dynamic stage,
which mirrors prerender, where caches past `await connection()` aren't
executed at all. The check is written as exact equality on
`RenderStage.Dynamic` rather than a numeric `< Dynamic` comparison
because `RenderStage.Abandoned` is numerically higher than Dynamic but
semantically represents an aborted initial prospective render whose
pending caches still need to be timed out while the outer flow awaits
`cacheSignal.cacheReady()`. The 50s duration is shared with the
prerender path via a new `USE_CACHE_FILL_TIMEOUT_MS` constant and
carries a TODO to derive it from the user-configurable
`staticPageGenerationTimeout`.

The tests model a realistic migration hazard: a module-scoped in-flight
fetch dedupe loader (the hand-rolled variant of the documented
`React.cache`-based preload pattern) joined by a `'use cache'` function.
That configuration deadlocks because the outer fetch is parked on the
Dynamic stage in dev, or returns an intentionally hanging promise during
prerender, and the cache never fills.
@unstubbable unstubbable force-pushed the hl/use-cache-dev-timeout branch from 9f3a92b to 9f2e217 Compare April 21, 2026 13:06
@unstubbable unstubbable requested a review from lubieowoce April 21, 2026 13:14
@unstubbable
Copy link
Copy Markdown
Contributor Author

Failures in test/integration/create-next-app/package-manager/bun.test.ts are unrelated.

@unstubbable unstubbable merged commit 058de27 into canary Apr 21, 2026
335 of 338 checks passed
@unstubbable unstubbable deleted the hl/use-cache-dev-timeout branch April 21, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants