guide·intermediate·updated 2026-10-23

Next.js 15 to 16 Upgrade: Fix the Build and Silent Breaks

Upgrade Next.js 15 to 16 without breaking production. Run the codemod, fix the build errors it leaves, and catch the silent async params failure. Tested on a sample app.

nextjsupgradejavascriptreactdevopsmigrationfoundersnode

Next.js 16 is the current major version. Teams on 15 need to move. Secondary reports say the October 2026 security fixes ship in 16.4.1 and 15.5.28. We did not fetch Next.js's own security post, so confirm the fixed versions there before you pin. The upgrade guide lists many changes, but in practice three of them matter for most apps: the async request APIs, the middleware to proxy rename, and parallel route slots that need a default.js file.

Next.js's docs recommend running the upgrade through a coding agent (npx next@canary upgrade --agent=latest). This guide uses the codemod path, which we tested. The codemod handles some of the work, but its default run also made one change we had to undo. Below is what we ran, what broke, and the exact fixes. We tested every step on a sample Next.js 15.5.28 app, upgraded it to 16.4.1, and checked the routes over HTTP. We ran it on Node.js 22.22.2. Next.js 16 requires Node.js 20.9 or later, but we did not test on Node 20.

What you need: Node.js 20.9 or later, a git-clean working tree (or a backup), and a staging environment to smoke-test.


Step 1: Snapshot and Confirm Your Starting Version

Commit or copy the project first. The codemod edits files in place.

git status            # must be clean, or make a backup copy
npx next --version    # confirm you are on 15.x

Next.js 16 also requires TypeScript 5.1 or later if you use TypeScript.

Step 2: Run the Upgrade Codemod, Without Adoption Codemods

Run the upgrade codemod with the --skip-adoption flag:

npx @next/codemod@canary upgrade 16.4.1 -y --skip-adoption

The -y flag accepts every prompt's default. --skip-adoption appears in the CLI's --help output, but the codemods page does not list it, so check npx @next/codemod@canary upgrade --help on your version. We ran the codemod without --skip-adoption first, and it did not produce a working app. Its adoption step added this line to every route file it found:

// @next-codemod-ignore Cache Components adoption: this segment temporarily allows blocking.
export const instant = false;

The codemods page describes this line as an adoption step for enabling cacheComponents later. With cacheComponents turned off, the next build fails on every one of those files:

Error: Route segment config "instant" requires `nextConfig.cacheComponents` to be enabled.

The --skip-adoption run avoids this. It still applied the middleware rename (Step 3). Our sample app had no unstable_ prefixes or experimental_ppr, so we did not test those codemods.

The codemod also moved React to 19.3.1 in our test app. Check that your React version matches what your UI libraries support before you deploy.

Step 3: Rename middleware to proxy (Node Runtime Only)

The codemod renamed middleware.js to proxy.js and the export to proxy:

// proxy.js
import { NextResponse } from 'next/server'
export function proxy(request) { return NextResponse.next() }
export const config = { matcher: ['/blog/:path*'] }

The build output then shows ƒ Proxy (Middleware). Two things to check by hand:

  • Edge middleware cannot move. The proxy file runs only on the Node.js runtime. If your middleware sets export const runtime = 'edge', keep it as middleware. The upgrade guide says this explicitly.
  • Renamed config flags. skipMiddlewareUrlNormalize is now skipProxyUrlNormalize. The codemod updates this flag, but grep your next.config to be sure.

Step 4: Fix the Parallel Route Slots (Build Fails Without This)

Every parallel route slot now needs a default.js file. Without it, the build fails:

Error: Parallel route slots cannot render the same URLs
- /actions-demo is missing a matching page or default.tsx in @modal

Add a default file that returns null (or call notFound() if you want a 404 when the slot has no match):

// app/@modal/default.js
export default function Default() {
  return null
}

After adding it, the build passed.

Step 5: Make Every params and searchParams Access Async (Silent Failure)

This is the change that causes silent breaks. In Next.js 16, synchronous access to params, searchParams, cookies(), headers(), and draftMode() is removed. The build does not fail. The value is simply missing.

We tested a page that reads params.id without await:

// app/sync-test/[id]/page.js (Next.js 16, sync access)
export default function Page({ params }) {
  return <h1>id={params.id}</h1>
}

The build passed. The page rendered <h1>id=</h1> with the id missing. A version of this page that awaits params rendered the value correctly.

The fix for every page, layout, route, and image function:

// app/blog/[slug]/page.js
export default async function Page({ params, searchParams }) {
  const { slug } = await params
  const q = await searchParams
  return <h1>Post {slug} {q.x}</h1>
}

The codemod for this is separate from the upgrade codemod:

npx @next/codemod@canary next-async-request-api .

Run it, then search your code for the APIs that still need await. Use this search and review each hit:

grep -rnE "params\.|searchParams\.|cookies\(\)\.|headers\(\)\." app/ --include=*.js --include=*.jsx --include=*.ts --include=*.tsx

The opengraph-image, twitter-image, icon, apple-icon, and sitemap functions are also affected. Their params and id arguments became promises in 16.

Step 6: Update Revalidation Calls

revalidateTag now takes a second argument, a cache life profile. Pass 'max' to keep stale-while-revalidate behavior:

// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
 
export async function savePost(id: string) {
  revalidateTag(`post-${id}`, 'max')
}

For immediate expiry in a Server Action, use updateTag instead. The upgrade guide says the one-argument form is deprecated and produces a TypeScript error. We did not test the TypeScript error, because our sample app is JavaScript. The JavaScript build passed with the one-argument form, so the error is a type-check error, not a runtime error. Run your type check (npx tsc --noEmit) to find these.

Step 7: Remove Config Options That No Longer Exist

Next.js 16 removed serverRuntimeConfig and publicRuntimeConfig. Our sample app still had them, and the build printed this warning:

⚠ Invalid next.config.js options detected:
⚠     Unrecognized key(s) in object: 'serverRuntimeConfig', 'publicRuntimeConfig'

The warning is not a build failure. The upgrade guide says both options were removed. We did not test a getConfig() call, so check any code that uses it. Move those values to environment variables:

  • Server-only values: read process.env.DATABASE_URL directly in Server Components.
  • Client values: rename them with the NEXT_PUBLIC_ prefix. One more trap: environment variables read at build time get baked into static pages. Our test page that read process.env.DB_URL rendered an empty value, because the page was prerendered during next build and the variable was not set then. To read a variable at request time, call connection() first:
import { connection } from 'next/server'
 
export default async function Page() {
  await connection()
  return <p>{process.env.RUNTIME_CONFIG}</p>
}

Also remove these, which the upgrade guide lists as removed: next lint (and the eslint config key), AMP, experimental.dynamicIO, experimental.useCache, and unstable_rootParams. Our test app did not use these, so we did not run them. Check each one with grep.

Step 8: Check Image Behavior

Next.js 16 changed several image defaults. These do not break the build, but they change what users receive:

Setting Old default New default Impact
minimumCacheTTL 60 seconds 4 hours Images update less often if the origin sends no cache headers
qualities all [75] quality props other than 75 get coerced to 75
maximumRedirects unlimited 3 Long redirect chains fail
Local IP optimization allowed blocked Set dangerouslyAllowLocalIP: true only on a private network

If you need the old cache behavior, set it back in next.config. We did not run image requests in this test, so verify each one against your own images.

Step 9: Build and Smoke-Test

Run the build, then start the server and check each route type. Our final test build passed with these routes:

npx next build
PORT=3000 npx next start -p 3000 &
curl -s -o /dev/null -w "%{http_code}\n" localhost:3000/
curl -s "localhost:3000/blog/hello?x=1" | grep -o '<h1>.*</h1>'
curl -s -o /dev/null -w "%{http_code}\n" localhost:3000/api/hello

The grep for the blog route should return <h1>Post <!-- -->hello<!-- --> <!-- -->1</h1>. The <!-- --> markers are React text separators, not errors. If the slug is missing, the sync-params problem from Step 5 is back.

Stop the server with its PID. Do not use pkill -f "next start" in a shell that has that string in its own command line, because it can kill the shell.

Step 10: Keep a Rollback Path

Pin the old version in your lockfile before you deploy, so you can revert in one step:

git revert <upgrade-commit>
npm ci

Deploy to staging first. Watch for 404s on dynamic routes and empty values in page output. Those are the two symptoms of the silent breaks in Step 5 and Step 7.


Common Mistakes

  • Running the codemod without --skip-adoption. It adds export const instant = false to every route file. The build then fails unless you enable cacheComponents.
  • Skipping the parallel route defaults. The build fails with "Parallel route slots cannot render the same URLs." Add default.js to every slot.
  • Leaving sync params in place. The build passes, but the value is empty. Grep for it.
  • Assuming middleware renamed cleanly. Edge middleware stays as middleware.
  • Reading environment variables at build time. They get baked into static pages. Use connection() for runtime values.

What We Tested

Step Result on our sample app
Next.js 15.5.28 build Passed
Upgrade codemod, default flags Build failed on instant lines
Upgrade codemod, --skip-adoption Codemods applied; build failed on parallel routes
default.js added to @modal Build passed
Sync params.id Built, rendered empty value
Async params Rendered correct value
/api/hello route 200
serverRuntimeConfig in config Build warning; values dropped
Node.js version 22.22.2 only; Node 20.9 not tested
TypeScript revalidateTag error Not tested (JavaScript sample)
next lint removal, image defaults Not tested; from the upgrade guide

Sources and Further Reading

  • The Next.js 16 upgrade guide covers every breaking change, including the image defaults and removed APIs.
  • The Next.js codemods page lists what each codemod changes.
  • Migrating to Cache Components is the next step if you want the new caching model. The upgrade guide and codemods page link to it. We could not fetch it during this check.
  • The Next.js 16 release security posts were not fetched. The fixed-version claim in the intro comes from secondary reports. Test environment: Next.js 15.5.28, upgraded to 16.4.1, React 19.3.1 (set by the codemod), Node.js 22.22.2.

Got stuck, or want this shipped end-to-end for you? bitroot.club builds custom products for founders. →