There is a special kind of framework bug that does not care where you deploy. Next.js 16.3 shipped one: combine a deployment adapter with output: 'standalone' and next build crashes, every time, on every platform — including Vercel itself. The identical config passes on 16.2.x.
If you self-host Next.js — Docker images built from .next/standalone, AWS via cdk-nextjs, a Bun adapter, or your own PaaS matching Vercel's Next.js support — this is the one 16.3 regression you need to understand before you upgrade. Here is the exact failing combination, the two-sided root cause, who has an escape hatch and who does not, and the playbook for getting past it.
The one-line config combo that crashes every 16.3 build
The trigger is narrow and fully reproducible. You need all three of these at once:
- Next.js 16.3.x with the default Turbopack builder,
- a deployment adapter configured (via
adapterPathinnext.config, or injected throughNEXT_ADAPTER_PATH), output: 'standalone'in the same config.
With that combination, every build dies in the standalone finalizer with:
Error: ENOENT: no such file or directory, open '<distDir>/next-server.js.nft.json'The minimal repro is almost insultingly small: a two-line stub adapter ({ name, async onBuildComplete() {} }), output: 'standalone', and a single pages/index.js. That config builds fine on 16.2.x and crashes on 16.3.0. The surviving upstream tracker is vercel/next.js#96646; the original report with the full root-cause chain, #96657, was auto-closed by a repro-link bot.
Note the blast radius carefully: this is not a self-hosting-only problem. Vercel's own builder injects its deployment adapter via NEXT_ADAPTER_PATH under the NEXT_ENABLE_ADAPTER rollout — so every Vercel deployment of a standalone-configured app on 16.3 breaks too. When the framework vendor's own platform trips over a regression, self-hosters should assume they are affected until proven otherwise, not the other way around.
Writer skipped, reader kept: the two-sided root cause
The crash is a writer/reader disagreement across the Rust–JS boundary inside the build:
| Side | Component | Behavior on 16.3 with an adapter |
|---|---|---|
| Writer (Rust/Turbopack) | Whole-app server NFT emission | PR #93684 skips emitting next-server.js.nft.json / next-minimal-server.js.nft.json whenever an adapter is configured ("Adapters don't read these files") |
| Reader (JS) | copyTracedFiles standalone finalizer | Reads distDir/next-server.js.nft.json unconditionally whenever standalone output is requested — with no .catch, unlike its sibling per-page trace reads |
The build runs adapter finalize and standalone finalize in sequence. With both configured, the Rust side skips the writer while the JS side still runs the reader: raw ENOENT, no fallback, no warning.
The "adapters don't read these files" assumption was true for adapters — which consume the per-endpoint NFTs — but the whole-app NFT has a second, adapter-independent consumer: the standalone finalizer. This is a recurring shape of bug, not a one-off. PR #94197 had already walked the same gate's assumption back once, when cache-handler tracing surfaced as another consumer the gate did not know about. Each consumer of a shared build artifact is load-bearing; gating emission on "the one consumer I know about doesn't need it" breaks every consumer you forgot.
Why a guard alone would ship broken servers
The obvious quick fix — wrap the copyTracedFiles read in a .catch so the build exits 0 — would be worse than the crash. Measured on a production monorepo app (16.3.0, Turbopack, standalone, Vercel adapter injected): with only the JS guard, the build succeeds but .next/standalone is missing 1017 of 2133 files (48%) versus a no-adapter control — including next/dist/server/next.js, next-server.js, and the rest of the server runtime. node server.js then fails immediately with Cannot find module.
A silently unbootable standalone directory trades a loud build failure for a runtime one. Restoring emission is the fix; the guard is defense-in-depth. That measurement is worth remembering the next time someone proposes "just catch the error" for a missing-artifact crash: first check whether the artifact was load-bearing.
The combination was never covered by CI. The next-server-nft test file has a with output:standalone suite and a with adapters suite, but no combined suite — the only state where the writer gate and the reader disagree. Pairwise coverage of interacting build flags would have caught this before release.
Who's hit, who's fine, and who has no escape
Not every 16.3 user is affected, and among the affected, the escape options are uneven. Here is the matrix:
| Setup | Hit? | Escape hatch |
|---|---|---|
Vercel deploy + output: 'standalone' | Yes — the builder injects its own adapter | Drop standalone from config on Vercel (the standalone directory is ignored there anyway); Next's own team did exactly this in a fixture, noting it is "meaningless on Vercel deployments" |
| cdk-nextjs on AWS | Yes — the adapter force-sets output: 'standalone' and the construct requires .next/standalone | None at config level. Both sides of the conflict are mandatory |
| Adapters that force-set standalone (e.g. next-adapter-bun) | Yes | None at config level. next-adapter-bun capped its peer range to next >=16.2.12 <16.3.0 and cannot build on 16.3.x at all |
| Adapter only, no standalone | No | — |
| Standalone only, no adapter | No | — |
The cdk-nextjs row is the one that should worry self-hosters most. When one side of a conflict is force-set by your adapter and the other is required by your infrastructure construct, there is no YAML edit that saves you. Your options collapse to: pin the framework, patch the adapter, or wait on upstream. That is precisely the position AWS self-hosters are in until the fix ships in a stable release.
The upstream fix and what to do today
The upstream fix, PR #97287, re-scopes the #93684 gate instead of removing it: skip the whole-app server NFTs only when an adapter is configured and standalone output is not. The is_standalone flag was already computed in the same function two lines below the gate, so the change is a reorder plus one condition — no new plumbing. Adapter-only builds keep the new no-emission behavior; adapter-plus-standalone falls through to the existing emission branches, restoring pre-16.3 behavior for exactly the broken combination.
The PR additionally gives copyTracedFiles the same .catch plus Log.warn treatment its sibling per-page reads already have, so any future writer/reader drift degrades into one actionable warning instead of a raw ENOENT — and it adds the missing combined with adapters and output:standalone CI suite that would have caught the regression in the first place.
Until a stable release containing the fix is in your hands, the ranked playbook is:
- Pin
16.2.xnow. If your lockfile already floated to 16.3, pin back. This is the only action with zero downside. - Verify the fix release before unpinning. Confirm the stable release notes (or the backport) include #97287, then upgrade a preview environment first and check that
.next/standaloneis complete — count the files or bootnode server.js, don't just check the exit code. - Carry an adapter-compat matrix per Next minor. Record which adapter versions you have qualified against which Next minor (Vercel builder, cdk-nextjs, OpenNext, Bun, Cloudflare). Treat an unqualified cell as "do not deploy," not "probably fine."
- Add the combined build to CI. A stub-adapter-plus-standalone build takes seconds and covers the exact state upstream CI missed. If the framework's matrix has a hole, your pipeline should fill it for your own stack.
One more edge worth knowing, flagged as out of scope on the fix PR: adapterPath: '' currently disables the adapter in JS (truthiness gates) while enabling the NFT skip in Rust (adapter_path.is_some() matches Some("")) — reproducing the same crash with no adapter at all. If you template next.config and an empty adapter path can leak through, normalize it to undefined.
Why "which Next minor" is now a platform-support question
Step back from this specific ENOENT and the structural lesson is bigger than one bad gate. In the pre-adapter era, self-hosting Next.js meant one blessed path — output: 'standalone', Docker, done — and a framework minor bump was a changelog to skim. In the OpenNext/adapter era, what determines whether your deploy works is the framework-plus-adapter matrix: the Next minor, the adapter, the builder injection on your platform, and the interactions between flags that no single party's CI covers in combination.
That turns "which Next minor do we run" from a tenant choice into a platform-support question. A self-hosted PaaS matching Vercel's Next.js support can no longer just track the framework release notes; it has to qualify each minor against each supported adapter, because the break will land in the interaction — an adapter the builder injects silently, a trace file one side stopped emitting, a construct that requires the directory the other side stopped filling. The 16.3 standalone break is the worked example: two features that each passed their own suite, combined in the one state nobody tested, breaking deploys on every platform at once.
Pin 16.2, qualify the matrix, and make the combined build a permanent CI fixture. The next interaction bug is already in someone's minor release — the only question is whether your pipeline catches it before your tenants do.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



