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.xNext.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-adoptionThe -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
proxyfile runs only on the Node.js runtime. If your middleware setsexport const runtime = 'edge', keep it asmiddleware. The upgrade guide says this explicitly. - Renamed config flags.
skipMiddlewareUrlNormalizeis nowskipProxyUrlNormalize. The codemod updates this flag, but grep yournext.configto 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 @modalAdd 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=*.tsxThe 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_URLdirectly 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 readprocess.env.DB_URLrendered an empty value, because the page was prerendered duringnext buildand the variable was not set then. To read a variable at request time, callconnection()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/helloThe 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 ciDeploy 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 addsexport const instant = falseto every route file. The build then fails unless you enablecacheComponents. - Skipping the parallel route defaults. The build fails with "Parallel route slots cannot render the same URLs." Add
default.jsto every slot. - Leaving sync
paramsin place. The build passes, but the value is empty. Grep for it. - Assuming
middlewarerenamed cleanly. Edge middleware stays asmiddleware. - 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. →