Upstream change
Next.js commit 5817bd1 ("Anchor the async local storage instances to global symbols", #97255) changes how Next.js constructs its core AsyncLocalStorage instances.
Instead of each *-async-storage-instance.ts calling createAsyncLocalStorage() at module scope, they now call a new getOrCreateGlobalAsyncLocalStorage(name) helper that stores the instance on globalThis under a version-keyed global symbol:
export function getOrCreateGlobalAsyncLocalStorage<Store extends {}>(
name: string
): AsyncLocalStorage<Store> {
const key = Symbol.for(`@next/${name}@${process.env.__NEXT_VERSION}`)
const globalStore = globalThis as typeof globalThis & {
[key: symbol]: AsyncLocalStorage<Store> | undefined
}
return (globalStore[key] ??= createAsyncLocalStorage<Store>())
}
Applied to: work-async-storage, work-unit-async-storage, action-async-storage, after-task-async-storage, console-async-storage, dynamic-access-async-storage, and request-insights-identity-storage.
Why it matters
These storages must be singletons within a realm. A store entered through one reference to a storage must be readable through every other reference to it; otherwise code running inside the scope sees no store at all. Module identity does not guarantee this — a realm can evaluate the same file more than once when it is reachable through more than one path, and each evaluation creates its own storage. A global symbol keeps the singleton intact across any number of duplicate module copies. Worker threads / edge sandboxes still get separate storages because each has its own globalThis.
Upstream notes this concretely broke in next dev due to a Node fs.realpathSync bug (nodejs/node#65113) that returns symlink-unresolved paths on pnpm installs, causing next/dist/... files to be evaluated twice.
Relevance to vinext
This is directly relevant to vinext's architecture. Per AGENTS.md ("RSC and SSR Are Separate Vite Environments"), the RSC and SSR environments are separate Vite module graphs with separate module instances. Any per-request state built on AsyncLocalStorage is at risk of the exact failure mode this upstream change guards against: duplicate module evaluation yielding multiple ALS instances, so a store set through one reference is invisible through another, and request-scoped context (headers, cookies, params, work store) silently reads as empty.
vinext should adopt the same robustness pattern — anchoring its async storage singletons to version-keyed global symbols — so its request-scoped context survives:
- separate RSC/SSR Vite environments,
- pnpm symlink /
realpathSync duplicate-evaluation cases,
- multiple copies of the shim modules reachable via different resolution paths.
Suggested work
- Audit vinext's AsyncLocalStorage usage (shims for
next/navigation, work/work-unit storage, request context setters passed across the RSC→SSR boundary).
- Introduce a
getOrCreateGlobalAsyncLocalStorage(name) equivalent that keys on globalThis with a version-scoped Symbol.for(...), and route instance creation through it.
- Add a test that simulates duplicate module evaluation (two module instances) and asserts the store is shared.
Reference
Upstream change
Next.js commit
5817bd1("Anchor the async local storage instances to global symbols", #97255) changes how Next.js constructs its core AsyncLocalStorage instances.Instead of each
*-async-storage-instance.tscallingcreateAsyncLocalStorage()at module scope, they now call a newgetOrCreateGlobalAsyncLocalStorage(name)helper that stores the instance onglobalThisunder a version-keyed global symbol:Applied to:
work-async-storage,work-unit-async-storage,action-async-storage,after-task-async-storage,console-async-storage,dynamic-access-async-storage, andrequest-insights-identity-storage.Why it matters
These storages must be singletons within a realm. A store entered through one reference to a storage must be readable through every other reference to it; otherwise code running inside the scope sees no store at all. Module identity does not guarantee this — a realm can evaluate the same file more than once when it is reachable through more than one path, and each evaluation creates its own storage. A global symbol keeps the singleton intact across any number of duplicate module copies. Worker threads / edge sandboxes still get separate storages because each has its own
globalThis.Upstream notes this concretely broke in
next devdue to a Nodefs.realpathSyncbug (nodejs/node#65113) that returns symlink-unresolved paths on pnpm installs, causingnext/dist/...files to be evaluated twice.Relevance to vinext
This is directly relevant to vinext's architecture. Per
AGENTS.md("RSC and SSR Are Separate Vite Environments"), the RSC and SSR environments are separate Vite module graphs with separate module instances. Any per-request state built on AsyncLocalStorage is at risk of the exact failure mode this upstream change guards against: duplicate module evaluation yielding multiple ALS instances, so a store set through one reference is invisible through another, and request-scoped context (headers, cookies, params, work store) silently reads as empty.vinext should adopt the same robustness pattern — anchoring its async storage singletons to version-keyed global symbols — so its request-scoped context survives:
realpathSyncduplicate-evaluation cases,Suggested work
next/navigation, work/work-unit storage, request context setters passed across the RSC→SSR boundary).getOrCreateGlobalAsyncLocalStorage(name)equivalent that keys onglobalThiswith a version-scopedSymbol.for(...), and route instance creation through it.Reference
realpathleaving symlinks unresolved after an unrelated stat nodejs/node#65113