Skip to content

Turbopack 16.3: postcss and webpack-loaders Node.js pools race to overwrite one [turbopack]_runtime.js, breaking cold builds (follow-up to #96591) #96619

Description

@finnan444

Link to the code that reproduces this issue

https://github.com/finnan444/nextjs-turbopack-runtime-pool-race

To Reproduce

  1. A Turbopack next build project with both a postcss.config.js and any .scss file. The linked repro is six source files: app/layout.js, app/page.js, one CSS module, one SCSS module, an empty postcss.config.js, package.json.
  2. Build with a cold .next.
  3. In a large enough app, cold builds fail intermittently:
Turbopack build failed with 7 errors:
./src/styles/<font>.module.css
Error: Error evaluating Node.js code
TypeError: __turbopack_context__.a is not a function
    at mod (postcss.config.js_.loader.mjs:11:31) [.next/build/chunks/[root-of-the-server]__<hash>._.js:56:30]

Every module that goes through postcss fails; which modules, and how many, differs run to run.

#96591 reports the same failure and identified the trigger, PR #94376, along with the
.cjs workaround. It was closed automatically because its reproduction link was not live yet
at filing time. Opening this as a fresh issue per the bot's instructions, with the repro public
up front and with an explanation of the intermittency that report does not cover.

Current vs. Expected behavior

Current. A build with a postcss config and any SCSS spawns two Turbopack Node.js transform
pools, postcss and webpack-loaders, and both write their runtime to the same path,
.next/build/chunks/[turbopack]_runtime.js. Since PR #94376 ("Smaller runtime size" in the
16.3 blog post) ships top-level-async support only
when async_module_info is non-empty, the two variants are no longer identical: the
webpack-loaders variant omits contextPrototype.a = asyncModule.

The generated postcss.config.js_.loader.mjs uses top-level await, so its chunk calls
__turbopack_context__.a(...). Whichever pool writes the shared file last decides whether the
helper is there - so the same source tree builds or fails nondeterministically.

ab.sh in the repro shows the two variants deterministically, by running one pool at a time:

build pool runtime size asyncModule occurrences
SCSS import removed postcss only 36110 B 2
postcss.config.js removed webpack-loaders only 31680 B 0
default both, same output path last writer wins n/a

The same two variants appear in the real app where we hit this (36116 B / 31686 B - the 6-byte
delta is longer paths). Failing builds there have the smaller, helper-less runtime.

loader.sh shows the other half, also deterministically - renaming the identical config file
.js -> .cjs removes the async loader entirely:

config chunks referencing an async config loader loader emitted asyncModule in runtime
postcss.config.js 4 postcss.config.js_.loader.mjs 2
postcss.config.cjs 0 none 0

Expected. The shared runtime contains every helper the chunks written against it reference -
or each pool writes its own runtime file.

Notes

  • Workaround (from Turbopack 16.3: __turbopack_context__.a is not a function when evaluating postcss.config.js #96591, confirmed here): rename postcss.config.js -> .cjs or .mjs.
    Turbopack only emits the async _.loader.mjs for configs whose path ends in .js.
  • A warm .next masks the bug - a leftover runtime may already contain the helper. We hit it
    after making CI key its .next cache by Next.js version, which made every CI build cold.
  • Caveat on the repro: ab.sh and loader.sh prove both halves of the mechanism
    deterministically, but the minimal project does not lose the race - the postcss pool
    consistently writes last there, so repro.sh (N cold builds) stays green. We could not shrink
    the race itself: 40+ mixed CSS/SCSS routes, App Router vs Pages Router, SCSS-first vs CSS-first
    module graphs, a lone dynamically imported SCSS, and an npm-workspaces monorepo layout all
    stayed green. In our real app (Pages Router, pnpm monorepo, Panda CSS + SCSS) it fails on
    roughly 2 of 9 cold builds on 16.3.0, and 3 of 4 on 16.3.1-canary.0 - the rate seems sensitive
    to machine load.
  • Present in 16.3.0 and 16.3.1-canary.0. 16.2.12 is unaffected.

Suggested fix

As in #96591: for the Node.js evaluate runtimes, either always include top-level-async support,
or ensure the virtual _.loader.mjs lands in async_module_info before runtime assembly, or
avoid top-level await in that loader. Additionally, having the two pools write to distinct paths
would remove the nondeterminism regardless.

Provide environment information

Operating System: macOS (Darwin 25.6.0), arm64
Node.js: v24.17.0
npm: 11.13.0 (repro) / pnpm 11.9.0 (real app)
Next.js: 16.3.0 and 16.3.1-canary.0
React: 19.2.0
sass: 1.94.0 (repro) / 1.77.0 (real app)

Which area(s) are affected?

Turbopack

Which stage(s) are affected?

next build (local), next build (Vercel/CI)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions