Next.js 16 Breaking Changes: What Actually Breaks (And How to Fix It)
The seven changes most likely to break your build when you upgrade — with real fixes, not just a changelog summary.
Every pattern in this guide is live in this site's own Next.js 16 codebase — not copied from the changelog.
Next.js 16 removes several things without a deprecation warning: synchronous access to params/cookies/headers/searchParams is gone entirely, middleware.ts must be renamed to proxy.ts, Turbopack is now the default build tool and fails outright if it finds a webpack config, and revalidateTag() now requires a second argument or it's a TypeScript error. This guide covers the seven changes most likely to break an upgrade, with working fixes for each.
1. Async Request APIs: No More Sync Escape Hatch
Next.js 15 made cookies(), headers(), draftMode(), and route params/searchParams asynchronous, but kept a temporary synchronous compatibility layer so existing code wouldn't break immediately. Next.js 16 removes that compatibility layer entirely. Synchronous access now throws instead of warning.
This affects params and searchParams in page.js, layout.js, route.js, default.js, and the metadata image conventions (opengraph-image, twitter-image, icon, apple-icon).
// Next.js 14 — synchronous, no longer works
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}
// Next.js 16 — params is a Promise, must be awaited
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}This exact pattern is live across every dynamic route on this site — blog posts, guides, and the tools catalog all destructure params after awaiting it:
// src/app/blog/[slug]/page.tsx (this site, in production)
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
}
export default async function BlogPostDetailPage({ params }: BlogPostPageProps) {
const { slug } = await params;
const post = getBlogPostBySlug(slug);
// ...
}Fastest fix: run npx next typegen (available since Next.js 15.5) to generate the PageProps, LayoutProps, and RouteContext type helpers, which give you a fully type-safe async signature without hand-writing the Promise<...> wrapper on every route.
2. middleware.ts Is Now proxy.ts
The middleware filename and named export are deprecated in favor of proxy — a rename meant to clarify that this file operates at the network-boundary/routing layer, not as general request middleware.
# Rename the file
mv middleware.ts proxy.ts// Before
export function middleware(request: Request) { /* ... */ }
// After
export function proxy(request: Request) { /* ... */ }One real constraint, not just a rename: the edge runtime is not supported in proxy — its runtime is fixed to nodejs and cannot be configured. If your middleware specifically depends on the edge runtime, you need to keep using the deprecated middleware filename until Next.js ships further edge-runtime guidance for proxy.
Config flags containing "middleware" are renamed too — for example skipMiddlewareUrlNormalize becomes skipProxyUrlNormalize in next.config.ts. The official upgrade codemod (below) handles both the rename and the flag updates automatically.
3. Turbopack by Default: The Webpack-Config Trap
Turbopack is stable and now runs by default for both next dev and next build — no --turbopack flag required. If your package.json scripts still pass --turbopack explicitly, it's harmless but unnecessary:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}The trap: if your project has a custom webpack configuration in next.config.ts and you run next build — which now uses Turbopack by default — the build fails outright to prevent silently ignoring your webpack setup. This is a deliberate hard stop, not a warning.
Three ways out:
- Use Turbopack anyway:
next build --turbopackignores the webpack config and builds with Turbopack. - Migrate the config: move webpack-specific options to Turbopack-compatible equivalents (most common case:
experimental.turbopackmoved to a top-levelturbopackkey innext.config.ts). - Opt back out:
next build --webpackkeeps using Webpack.
If you see a failing build citing a webpack configuration you never wrote yourself, check for a plugin or dependency injecting one — that's the most common surprise trigger.
4. next/image: Three Silent Default Changes
next/image ships three changed defaults in v16 that don't error — they just quietly change behavior:
| Setting | Old default | New default | Why |
|---|---|---|---|
images.minimumCacheTTL | 60 seconds | 4 hours (14400s) | Images without a cache-control header were revalidating every 60s, driving up CPU/cost for content that rarely changes |
images.imageSizes | includes 16 | 16 removed | Retina displays (devicePixelRatio: 2) fetch a 32px image anyway, making the 16px entry mostly dead weight in the srcset |
images.qualities | all qualities allowed | [75] only | A quality prop outside the allowed list is silently coerced to the nearest configured value |
None of these throw — a quality={80} prop with the new default just quietly renders at 75. If a design review catches unexpected image softness after upgrading, check this first before assuming it's a rendering bug.
A fourth change is a real security control, not a performance tweak: local images with query strings (<Image src="/assets/photo?v=1" ... />) are now blocked unless explicitly allowed via images.localPatterns[].search — closing an enumeration-attack vector where an attacker could probe local filesystem paths through the image optimizer. Similarly, images.dangerouslyAllowLocalIP now defaults to blocking local/private IPs in the image optimizer entirely (name is a deliberate signal: only re-enable it on a genuinely private network), and images.maximumRedirects caps at 3 instead of following redirects indefinitely.
5. revalidateTag() Now Requires a Second Argument
revalidateTag(tag) — the single-argument form — is deprecated and now produces a TypeScript error, not just a runtime warning. It requires a cacheLife profile as the second argument:
// Before — TypeScript error in Next.js 16
revalidateTag("posts");
// After
revalidateTag("posts", "max");This is a real API-shape decision, not busywork: it forces you to be explicit about *how stale* is acceptable while the tag revalidates in the background (stale-while-revalidate semantics). If you need the old "expire immediately, no stale reads" behavior instead, that's a different function entirely — updateTag(), new in v16, is Server-Actions-only and gives read-your-writes semantics by expiring and refreshing within the same request:
"use server";
import { updateTag } from "next/cache";
export async function updateUserProfile(userId: string, profile: Profile) {
await db.users.update(userId, profile);
// User sees their own change immediately — no stale read
updateTag(`user-${userId}`);
}Rule of thumb: revalidateTag for content where a brief delay is fine (blog posts, catalogs); updateTag for anything the acting user needs to see change instantly (their own settings, their own form submission).
6. Parallel Routes Now Require default.js
Every parallel route slot (@modal, @sidebar, etc.) now requires an explicit default.js file. Builds fail without it — this one has no silent-degradation path.
// app/@modal/default.tsx
import { notFound } from "next/navigation";
export default function Default() {
notFound();
}Or, if you want the slot to simply render nothing rather than 404:
// app/@modal/default.tsx
export default function Default() {
return null;
}If your project doesn't use parallel routes at all, this doesn't apply — but it's an easy one to miss if a route group was added speculatively and never finished.
7. Quick Reference: Everything Else Removed in 16
A few more removals worth knowing about even if they don't hit every project:
- `next lint` is gone.
next buildno longer runs linting either. Use ESLint or Biome directly — this site's ownpackage.jsonalready reflects this: the lint script is plain"lint": "eslint", not"next lint". A codemod (npx @next/codemod@canary next-lint-to-eslint-cli .) automates the switch. - AMP support is fully removed —
next/amp,useAmp, and theampconfig key are gone. If your project still has AMP pages, this blocks the upgrade until they're removed or rearchitected. - `serverRuntimeConfig` / `publicRuntimeConfig` are removed. Use environment variables directly —
process.env.Xin Server Components for server-only values,NEXT_PUBLIC_-prefixed vars for anything the client needs. - `experimental.dynamicIO` and `experimental.useCache` are deprecated in favor of the top-level
cacheComponentsconfig option. - `next/legacy/image` is deprecated — use
next/image. `images.domains` is deprecated — useimages.remotePatternsfor tighter security scoping.
The Automated Path
Before doing any of this by hand, try the official codemod first — it handles the Turbopack config move, the middleware→proxy rename (including config flags), the async-API migration, and the next lint → ESLint CLI switch in one pass:
pnpm dlx @next/codemod@canary upgrade latestRun it, then work through whatever it can't handle automatically using the sections above.
Frequently Asked Questions
What breaks when upgrading from Next.js 15 to Next.js 16?
The biggest breaking change is that synchronous access to params, searchParams, cookies(), headers(), and draftMode() is fully removed — Next.js 15's temporary compatibility layer is gone, so any component still destructuring these synchronously will throw. Other breaking changes include the middleware-to-proxy rename, Turbopack becoming the default build tool (which fails builds that have an unmigrated webpack config), parallel routes requiring an explicit default.js, and revalidateTag() requiring a second cacheLife argument.
Do I need to rename middleware.ts to proxy.ts in Next.js 16?
The middleware filename and export are deprecated, not yet hard-removed, but renaming to proxy.ts (and the function to proxy) is the recommended path forward. One real constraint: the proxy convention only supports the nodejs runtime, not edge — if you specifically need the edge runtime, stay on the middleware convention until Next.js extends edge support to proxy.
Why does my Next.js 16 build fail with a webpack configuration error?
Next.js 16 uses Turbopack by default for next build. If next.config.ts contains a custom webpack() function, the build fails outright rather than silently ignoring it — a deliberate guard against misconfiguration. Fix it by migrating the config to Turbopack-compatible options, forcing next build --turbopack to ignore the webpack config, or explicitly opting out with next build --webpack.
Is there an automated way to migrate to Next.js 16?
Yes — running pnpm dlx @next/codemod@canary upgrade latest handles the turbopack config move, the middleware-to-proxy rename and its config flags, migration to async Request APIs, and the next lint-to-ESLint-CLI switch. It doesn't cover every manual decision (like choosing a cacheLife profile for revalidateTag calls), but it clears most of the mechanical work automatically.