Welcome to HowToShipIt — practical how-to guides for developers: code, AI tools, and servers, explained step by step.

Upgrade Next.js 15 to 16: The EOL Checklist That Won’t Break Your Build (2026)






Next.js 15 hits end of life on October 21, 2026 — two days from now. After that date, your app will not stop running, but no scheduled critical bug fixes or security patches will ship for the 15.x line ever again. The next disclosed vulnerability gets fixed only in 16.

So if you still run 15.x, the next few days are the cheapest time you’ll ever get to upgrade Next.js 15 to 16: the migration tooling is mature, the gotchas are documented, and the deadline is forcing your hand anyway. This guide walks through the safe order — the two codemods you actually need to run, the traps that break builds, and the version bumps to check first.

What Oct 21 actually means for your production app

Next.js’s official support policy keeps each major version in Maintenance LTS — critical bug fixes and essential security updates only — for two years after its initial release. Next.js 15 shipped October 21, 2024, so the two-year clock runs out this Wednesday.

What stops on Oct 21:

  • Security patches for 15.x — the October 14 out-of-band release (15.5.28, patching two critical RCE flaws) was almost certainly the last scheduled one.
  • Critical bug fixes — no more backports, no more maintenance releases on a plan.
  • Official support — 14.x and below are already EOL; 15 joins them.

Two caveats worth being honest about. First, the policy does let Vercel patch unsupported versions “in rare circumstances” for severe bugs — but that’s an escape hatch, not a plan you can rely on. Second, nothing gets switched off: your app keeps serving traffic. The risk is purely forward-looking — the next vulnerability goes unfixed on your line.

Step 0: know exactly what you’re running

Before touching anything, pin down your current state. You can’t plan a migration from vibes.

npm list next
npm ls sharp          # the Oct 14 RCE fix rides on sharp ≥ 0.35.5
node -v               # needs to be 20.9.0+ for Next.js 16
npx tsc --version     # needs 5.1.0+ for Next.js 16

Then check what’s deployed, not just what’s in your lockfile. On Vercel, open the project’s Deployments tab and confirm the build logs show the Next.js version you think they do. Self-hosted? Check your running container or server process — node_modules/next/package.json on the box, not the repo.

If you know you cannot complete the 16 upgrade before Wednesday, do this today instead: land on 15.5.28. It’s the latest 15.x release (shipped October 14, 2026 as part of the out-of-band security update), and it’s as safe as the 15 line will ever be again. Then treat the 16 upgrade as a normal sprint task rather than an emergency.

Target for the upgrade: 16.4.2 (the latest 16.x as of October 19, 2026). Check npm for the current latest when you run this — patch releases keep landing.

Step 1: run the first codemod (and know what it skips)

Next.js 16 ships an official upgrade codemod, and it’s genuinely good at the mechanical renames:

npx @next/codemod@canary upgrade latest

It migrates:

  • experimental.turbopack config → the new top-level turbopack key
  • next lint → the ESLint CLI (see step 6 — next lint is gone)
  • the deprecated middleware convention → proxy
  • unstable_ prefixes off stabilised APIs
  • the experimental_ppr route-segment config

Here’s the trap the official guide states plainly but most blog guides bury: the codemod does not run every migration codemod. Specifically, it does not migrate synchronous request APIs. If your codebase still uses sync params, searchParams, cookies(), headers(), or draftMode() — leftovers from the 15 compatibility period — those are fully removed in 16 and the first codemod won’t touch them.

That’s the single most common reason a “ran the codemod” upgrade still fails. Run the second codemod yourself:

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

The before/after is mechanical but easy to miss at scale:

// ❌ breaks in Next.js 16
export default function Page({ params }: { params: { id: string } }) {
  const id = params.id;
  // ...
}

// ✅ Next.js 16
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  // ...
}

While you’re at it, generate honest types for your routes. next typegen was introduced in 15.5 and produces PageProps, LayoutProps, and RouteContext helpers so your params types stop lying:

npx next typegen

Step 2: deal with the Turbopack default (the build-fail trap)

In Next.js 16, next build and next dev use Turbopack by default. That’s a win for most apps — but the docs include a deliberate tripwire: if your next.config has a custom webpack() key, the build now fails on purpose “to prevent misconfiguration issues.”

If your upgrade dies on the first build, this is probably why. You have three options, straight from the upgrade guide:

  1. next build --turbopack — build with Turbopack, ignoring the webpack config
  2. Migrate your webpack config to Turbopack options in the config file
  3. --webpack — opt out of Turbopack and keep your webpack pipeline

Also note the small cosmetic changes while you’re here: next dev now writes to .next/dev (separate from build output), and build output no longer shows the Size/First Load JS columns. Don’t panic when your familiar build summary looks different.

Step 3: the middleware → proxy rename is nodejs-only

The first codemod renames middleware.ts to proxy.ts and the middleware export to proxy. It also renames skipMiddlewareUrlNormalize to skipProxyUrlNormalize. That’s fine — unless you run middleware on the edge runtime.

From the upgrade guide, verbatim: “The edge runtime is NOT supported in proxy. The proxy runtime is nodejs, and it cannot be configured.”

This is a real behavioural break for a specific group: if your middleware depends on the edge runtime — edge-local latency, edge KV reads, no cold-start tolerance — do not rename it. The guide’s own checklist warns that “edge middleware is not blindly renamed to proxy.” Keep the middleware.ts filename and it keeps working.

If your middleware is a plain auth guard or redirect layer running fine on Node (and for most apps it is — see the Next.js middleware/proxy guide for the common patterns), accept the rename and move on.

Step 4: the things Next.js 16 removed outright

These are hard breaks, not deprecations. The upgrade guide confirms all of them:

  • AMP — every AMP API and config option is removed. If you still have AMP pages, they’re now plain pages.
  • next lint — the command is gone, and builds no longer lint automatically. The eslint config key is also removed; run ESLint (or Biome) directly in your workflow. A next-lint-to-eslint-cli codemod handles the migration.
  • serverRuntimeConfig / publicRuntimeConfig — removed; use environment variables instead.
  • Parallel routes without default.js — every slot now requires an explicit default.js, or the build fails. The standard content is notFound() or return null.
  • Single-argument revalidateTag — it now requires a cacheLife profile as the second argument, e.g. revalidateTag('posts', 'max'). The old single-argument form produces a TypeScript error.

Step 5: the silent config changes nobody warns you about

Beyond the loud breaks, the upgrade guide documents a set of default changes that alter behaviour without failing your build — the kind of thing that ships green and breaks on Tuesday:

  • images.minimumCacheTTL default changed from 60 seconds to 4 hours (14400s) — images update far less often than you expect.
  • images.qualities default is now only [75]; other quality props get coerced to it.
  • Local images with query strings (/assets/photo?v=1) now need an images.localPatterns.search config entry.
  • Local-IP image optimisation is blocked by default — the new images.dangerouslyAllowLocalIP flag exists for a reason, and VPC/split-DNS setups will see 400s without it.
  • images.maximumRedirects default is now 3 (previously unlimited).
  • Global scroll-behavior: smooth is no longer overridden during navigation — restore it with data-scroll-behavior="smooth" on your <html> element.

If your app serves user-uploaded images, has a split-DNS setup, or relies on animated scroll navigation, audit this list before you merge — these are the “it works on my machine” bugs that only appear in production.

Step 6: Cache Components are still opt-in (don’t flip them yet)

You will see a lot of advice framing Next.js 16 as “the PPR release.” It isn’t, quite. Cache Components are opt-in behind the cacheComponents: true flag in your config — enabling it can surface build errors for uncached data outside <Suspense>, so this is a separate, deliberate project, not part of your upgrade weekend.

Same story for 16.3’s stable Instant Navigations and Partial Prefetching: both are opt-in behind the cacheComponents and partialPrefetching flags and are planned as defaults in a future major version. Upgrade to 16 for the security line; adopt the caching model when you’ve measured it on a staging deploy.

The safe upgrade order, in one checklist

  1. npm list next — know your exact 15.x version; confirm the deployed build too.
  2. Land on 15.5.28 if you can’t upgrade this week — the safe parking spot.
  3. Bump to the latest 16.x (16.4.2 as of October 19, 2026) with Node 20.9.0+ and TypeScript 5.1.0+.
  4. Run npx @next/codemod@canary upgrade latest.
  5. Run npx @next/codemod@canary next-async-request-api . — the one the first codemod skips.
  6. npx next typegen for honest route types.
  7. Decide the webpack() question: migrate to Turbopack options, --turbopack, or --webpack.
  8. Check the edge middleware question: keep middleware.ts if you need the edge runtime.
  9. Add default.js to every parallel-route slot; fix revalidateTag calls; remove AMP and runtime configs.
  10. Audit the images.* defaults — especially minimumCacheTTL and local-IP optimisation.
  11. Full test pass + visual diff on staging before Wednesday. Or, if you use AI-assisted upgrades, the docs now recommend npx next@canary upgrade --agent=latest as the guided route.

EOL deadlines are easy to ignore because nothing breaks on the day. The thing that breaks comes later — a CVE announcement where the fix only lands on 16, and then you’re upgrading under real pressure instead of a calendar. Two days out, the cheapest possible moment is this one.

Further Reading & References

Leave a Comment