Skip to content

runtimeServerDeploymentId: prerendered flight payloads pin the build-time deployment id — serving a build under a different deployment id causes an infinite MPA reload loop #98078

Description

@cipriancaba

Link to the code that reproduces this issue

https://github.com/cipriancaba/next-runtime-deployment-id-prerender-skew

To Reproduce

  1. npm install
  2. NEXT_DEPLOYMENT_ID=dpl_aaaaaaaaaaaaaaaa npm run build
  3. NEXT_DEPLOYMENT_ID=dpl_bbbbbbbbbbbbbbbb npm run start (same build artifact, different deployment id — simulating a .next built under deployment A being served under deployment B, e.g. a Turborepo remote-cache restore across Vercel deployments)
  4. Open http://localhost:3000 (a fully prerendered page) and click the <Link> to /dynamic (a dynamic page)
  5. Observe in the Network tab: a full document navigation instead of a client-side RSC navigation. Go back to / and click again — it hard-reloads every time and never recovers. With a router.push in a form (login flows) this becomes an infinite hard-reload loop.
  6. Control: rerun step 3 with the same id as the build (dpl_aaaaaaaaaaaaaaaa) — the same click is a normal client-side navigation.

Browserless verification (server from step 3 running):

$ curl -s http://localhost:3000/ | grep -o 'dpl_[ab]*' | sort | uniq -c
  23 dpl_aaaaaaaaaaaaaaaa            # BUILD-time id baked into the served flight payload

$ curl -s -L -D - -o /dev/null -H 'RSC: 1' http://localhost:3000/dynamic | grep -i x-nextjs-deployment-id
x-nextjs-deployment-id: dpl_bbbbbbbbbbbbbbbb   # RUNTIME id on the RSC navigation response

Headless-browser check (check.mjs in the repro, Playwright counting full document navigations after clicking the <Link>):

# mismatched ids
round 1: navigation to /dynamic caused 1 full document load(s) -> MPA fallback (full page reload)
round 2: navigation to /dynamic caused 1 full document load(s) -> MPA fallback (full page reload)

# control, same id
navigation to /dynamic caused 0 full document load(s) -> client-side navigation

Current vs. Expected behavior

Current: experimental.runtimeServerDeploymentId (#86865, auto-enabled on Vercel when skew protection is on) resolves the deployment id at runtime for data-dpl-id and dynamic responses — but the id embedded in prerendered flight payloads (the b field consumed by setNavigationBuildId) is still baked at build time. When a .next built under deployment A is served under deployment B, the initial HTML of a prerendered route initializes the client's navigation build id with A, every dynamic RSC/nav response carries x-nextjs-deployment-id: B, and the client skew check (fetch-server-response.js / server-action-reducer.js: responseBuildId !== getNavigationBuildId()) fires on every client navigation → MPA full-page reload → the reload serves the prerendered HTML again with A's id → infinite hard-reload loop.

We hit this in production (Next 16.3.3) via a Turborepo remote-cache hit on Vercel builders after excluding VERCEL_DEPLOYMENT_ID from the task hash — the anti-pattern from vercel/turborepo#10478, since reverted on our side. Our login form (router.push before a server action) became unusable; wire traffic between a fresh and a restored deployment was byte-identical except each client sending its own deployment id.

Expected: the navigation build id in prerendered flight payloads resolved at serve time (like data-dpl-id already is), making build artifacts deployment-portable — or explicit documentation that .next artifacts must never be reused across deployments while skew detection is active.

Since data-dpl-id is already injected at serve time, serve-time stamping the prerendered payload's navigation build id would also unlock the Turborepo-cache-on-Vercel case that vercel/turborepo#10478 closes as a platform limitation (the "we need to let deployment platforms influence the hash" takeaway there — this is the other half of that story).

Aware of #94734 and #95397 as adjacent runtimeServerDeploymentId issues; this one is specifically about the prerendered flight payload id. Supersedes #98077, which was closed by the bot for a missing reproduction link — this issue adds the runnable reproduction.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.6.0: Fri Jul 31 19:19:08 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6050
  Available memory (MB): 49152
  Available CPU cores: 18
Binaries:
  Node: 24.14.1
  npm: 11.18.0
  Yarn: N/A
  pnpm: 10.33.0
Relevant Packages:
  next: 16.3.3 // Latest available version is detected (16.3.3).
  eslint-config-next: N/A
  react: 19.2.8
  react-dom: 19.2.8
  typescript: 5.9.3
Next.js Config:
  output: N/A

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

Navigation, Runtime

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

next build (local), next start (local), Vercel (Deployed)

Additional context

Verified on next@16.3.3 (latest stable) and next@16.4.0-canary.12 — identical behavior on both: served prerendered HTML carries the build-time id, the RSC navigation response header carries the runtime id, and Playwright observes the MPA full-page reload on every <Link> click.

Metadata

Metadata

Assignees

No one assigned

    Labels

    RuntimeRelated to Node.js or Edge Runtime with Next.js.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions