vercel / vercel/next.js

E696 `Invariant: Expected workUnitAsyncStorage to have a store` intermittently fails `next build` static export on random pages under machine load

Open
#95,545 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Runtime
Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://github.com/discordwell/next-e696-workunit-store-race

To Reproduce
  1. npm install
  2. npm run repro — loops next build --webpack (default 10×) under CPU spinners (RUNS=25 for more attempts)

This is a scheduling race, so reproduction is statistical: on an idle machine the loop stays green; it fires under genuine machine-wide CPU contention. Original environment: an 18-core darwin workstation under heavy background load (leaked containers + parallel builds) — 6+ failures in one day on a ~550-static-page production app, on a different trivial static page each time. 12 controlled rebuilds of the identical SHA on the same machine while idle were all green (including runs at reduced QoS under 16 CPU spinners — it takes real contention). The repo README carries full instructions and the source-level analysis.

Current vs. Expected behavior

Current: during Generating static pages, a random page intermittently fails with:

Error [InvariantError]: Invariant: Expected workUnitAsyncStorage to have a store. This is a bug in Next.js.

(__NEXT_ERROR_CODE: E696), and with defaults (single attempt + prerenderEarlyExit) the whole build fails. The failing page differs each occurrence (/auth/error, /auth/help, /assistant/activate, …) — all trivial fully-static pages.

Expected: static export succeeds regardless of machine load.

Provide environment information
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0
  Available memory (MB): 49152
  Available CPU cores: 18
Binaries:
  Node: 22.22.3
  npm: 10.9.8
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.1.6
  eslint-config-next: N/A
  react: 19.0.0
  react-dom: 19.0.0
  typescript: 6.0.3
Next.js Config:
  output: N/A
Which area(s) are affected? (Select all that apply)

Runtime

Which stage(s) are affected? (Select all that apply)

next build (local)

Additional context

Re-files #95543, which was auto-closed — it was created via the gh CLI rather than this form, so the validator could not read the reproduction-link field (the link existed in the body). Per the bot's instructions, this is a new issue with the link in the correct section.

Why it races (analysis against 16.1.6 dist; the same code is present in canary as of 2026-07-06, so this is not fixed by upgrading):

server/app-render/use-flight-response.jsgetFlightResponse memoizes the deserialized flight response per stream at first touch, and reads workUnitAsyncStorage.getStore() from whatever async context makes that first touch, throwing E696 when absent. The store is only functionally needed there for the prerender-client (canary: also validation-client) process.nextTick deferral — the throw is a context-loss tripwire.

Two windows let that first touch execute outside any workUnitAsyncStorage.run(store, …) scope:

  1. Abort-path unwind. app-render.js wraps the render calls in workUnitAsyncStorage.run(...), but the AbortController.abort() calls that terminate those phases (scheduled after waitAtLeastOneReactRenderTask()) execute in the enclosing scope, which carries no work-unit store. Abort listeners dispatch synchronously in the aborting context, so React's unwind work — which can reach useFlightStreamgetFlightResponse — runs store-less.
  2. Backpressure resume. React's streaming render pauses on backpressure and resumes from the consumer's pull(). During static export that consumer is the export worker's stream collector, outside the store scope. If a page's shell hasn't completed its first in-scope task before the consumer drives the stream, the memoized first touch lands store-less.

On an idle machine each page's shell completes inside its first in-scope render task, so the memoization always captures a live store and the bug never fires — which is why CI and Vercel builds look clean. Under CPU starvation, render tasks lag behind the abort/backpressure windows and a random page loses the race; a large static page count × export workers × per-page concurrency (8) gives every build many dice.

Suggested fix shape: carry the context across those seams explicitly — AsyncLocalStorage.snapshot()/.bind() around the abort handlers and stream handoffs, or pass the store as a parameter the way collectSegmentData already does (it receives the store explicitly precisely because ALS doesn't flow there).

Workaround (has absorbed every occurrence for us):

// next.config.mjs
experimental: {
  staticGenerationRetryCount: 3,
}

Related: #86978 (same invariant during prerender, Turbopack, "only occurred locally, not on Vercel"), #92656 (same signature, auto-closed for a missing repro link), #90669 (same invariant via a different trigger — module duplication in pnpm monorepos).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run the linked reproducer with npm install and npm run repro, then inspect server/app-render/use-flight-response.js and app-render.js, including the collectSegmentData pattern mentioned in the analysis. Trace the abort and stream handoff paths under CPU contention. Done means static export remains reliable under load without E696 failures.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nextjs, node.js, react
Domain
backend, build-system
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.