Full-Stack Development
Upgrading from Next.js 15 to 16.4 before end of life
Migrate from Next.js 15 to 16.4.2, apply October 2026 security patches, update async request APIs, and adopt Partial Prefetching.
By Tanvir Ahamed, published . 9 min read.
Researched and written with AI assistance; sources are linked below.
Next.js 15 reaches end of life on October 21, 2026, two years after its initial October 21, 2024 release (endoflife.date). After that date, the 15.x release line stops receiving bug fixes and security patches. Combined with the critical upstream vulnerabilities patched in the October 14, 2026 Next.js security update, teams still running Next.js 15 face an immediate maintenance cutoff. This guide walks through patching immediate exposures, running the automated codemods to upgrade to Next.js 16.4.2, resolving breaking API and bundler changes, and adopting Partial Prefetching and Cache Components.
Why the October 2026 deadline matters
Starting with version 14, Vercel follows a two-year Long-Term Support (LTS) schedule for each major release, split into Active LTS and Maintenance LTS (endoflife.date). Only the stable release channel is meant for production deployments. Next.js 16 entered Active LTS on October 21, 2025, and remains supported until October 2027, whereas Next.js 15 exits Maintenance LTS on October 21, 2026.
That lifecycle boundary arrives right after two major security advisories. Following an advance notice on October 8, 2026, the Next.js team published an out-of-band October 14, 2026 security update shipping v16.4.1 (Active LTS) and v15.5.28 (Maintenance LTS). Because earlier 15.x minor lines do not receive security backports, any application still on 15.0 through 15.4 must move to at least 15.5.28 or upgrade directly to 16.4.2.
The October 14 update resolves three upstream vulnerabilities:
- Critical Remote Code Execution in
use cache(GHSA-673q-mjm6-r5xr, ReactGHSA-xqc2-xf4c-x3h3): Affects App Router applications using Cache Components on Next.js16.0.0through16.4.0when ause cachefunction returns multiple typed arrays. There is no configuration workaround. - Critical Remote Code Execution in Image Optimization with SVGs (
CVE-2026-96889,GHSA-2464-fhr4-w9xw): A memory vulnerability inlibrsvg(prior to2.63.2, used viasharp) can lead to remote code execution onglibc-based Linux systems when processing SVG images. This affects Next.js10.0.0to15.5.27and16.0.0to16.4.0. Upgrading to15.5.28or16.4.1+pulls insharp0.35.5or later. - High-severity Denial of Service in Server Actions (
GHSA-p3m5-59mp-jv5r, ReactGHSA-h3gv-c5rc-p453): Crafted requests against App Router applications (13.0.0to15.5.27and16.0.0to16.4.0) that expose at least one Server Action can trigger excessive CPU usage and block request handling within the process.
These fixes arrived on the heels of the September 30, 2026 security release (v16.3.8 and v15.5.27), which patched seven issues including High-severity Server-Side Request Forgery (SSRF) when images.remotePatterns is configured, cross-route cache poisoning (CVE-2026-94484, GHSA-4jqv-mc3x-m676), nested 'use cache' root param leaks (GHSA-h694-7cp9-m8p3), and a next dev Model Context Protocol (MCP) origin check vulnerability. An earlier out-of-band patch on September 22 (v16.3.6 and v15.5.26) also resolved a critical RCE in next/og (GHSA-vcvr-r3jv-pc5j, tracked on the Next.js security advisories page).
If you need a same-day stopgap before starting the v16 migration, pin next@15.5.28 first. Otherwise, moving straight to next@16.4.2 covers the security advisories and keeps the application on Active LTS.
Running the Next.js 16 upgrade codemod and updating runtime dependencies
Before touching application code, check your runtime and compiler versions against the official Next.js 16 upgrade guide:
- Node.js: Minimum
20.9.0(Node.js 18 support is dropped). Because Node.js 20 reached its own end of life on April 30, 2026, target Node.js 22 LTS or Node.js 24 LTS in production. - TypeScript: Minimum
5.1.0. - React: Upgrade
reactandreact-dom(along with@types/reactand@types/react-dom) to React 19. Next.js 16.4 supports React 19.3 (Next.js 16.4 release notes). - Browser support: Chrome 111+, Edge 111+, Firefox 111+, and Safari 16.4+.
Next.js provides automated codemods that handle the bulk of the mechanical changes. Run the primary upgrade codemod and the asynchronous request API codemod from your project root:
# 1. Run the primary Next.js upgrade codemod
npx @next/codemod@canary upgrade latest
# 2. Convert synchronous params, searchParams, cookies(), and headers() calls
npx @next/codemod@canary next-async-request-api .
# 3. Pin Next.js 16.4.2 and latest React 19 packages
npm install next@16.4.2 react@latest react-dom@latest
# 4. Generate route-aware TypeScript helper types
npx next typegenThe upgrade latest codemod updates next.config.ts to hoist experimental.turbopack to the top-level turbopack key, migrates next lint scripts to the ESLint CLI, renames the deprecated middleware convention to proxy, strips unstable_ prefixes from stabilized APIs, and removes legacy experimental_ppr flags (Next.js 16 upgrade guide).
If you use AI coding assistants in your workflow, Next.js 16.4 also introduces an agent-assisted upgrade mode (Next.js 16.4 release notes). Running npx next@canary upgrade --agent=latest prepares the migration instructions and codemods for a coding agent to execute, and npx @next/codemod@canary agents-md configures AGENTS.md to reference the version-matched documentation bundled inside node_modules/next/dist/docs/.
Handling breaking API and configuration changes between Next.js 15 and 16
Once the codemods finish, inspect the areas where Next.js 16 removes deprecated v15 behavior (Next.js 16 upgrade guide).
Asynchronous request APIs
Next.js 15 deprecated synchronous access to request-specific data; Next.js 16 removes synchronous access completely. Now, params and searchParams passed to pages, layouts, route handlers, opengraph-image, twitter-image, icon, and apple-icon are strictly Promises. Likewise, cookies(), headers(), and draftMode() imported from next/headers must be awaited.
Update your dynamic route components to await both params and header helpers (you can also use the global PageProps helper generated by npx next typegen):
// app/projects/[slug]/page.tsx
import { cookies, headers } from 'next/headers';
type ProjectPageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export default async function ProjectPage(props: ProjectPageProps) {
const { slug } = await props.params;
const searchParams = await props.searchParams;
const cookieStore = await cookies();
const headerList = await headers();
const theme = cookieStore.get('theme')?.value ?? 'system';
const userAgent = headerList.get('user-agent') ?? 'unknown';
const tab = typeof searchParams.tab === 'string' ? searchParams.tab : 'overview';
return (
<main data-theme={theme}>
<h2>Project: {slug}</h2>
<p>Active tab: {tab}</p>
<small>Requested by: {userAgent}</small>
</main>
);
}In Client Components, unwrap params and searchParams Promises with React's use(params) hook instead of async/await.
Renaming middleware to proxy
Next.js 16 deprecates middleware.ts in favor of proxy.ts, clarifying its role at the network boundary (Next.js 16 upgrade guide). Rename the file at the root of your project (or inside src/) and export a proxy function:
// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('x-request-path', request.nextUrl.pathname);
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};If your configuration previously used experimental.middlewarePrefetch, rename that key to experimental.proxyPrefetch.
Turbopack by default and custom Webpack configurations
In Next.js 16, both next dev and next build run Turbopack by default without needing --turbopack or --turbo flags (Next.js 16 upgrade guide). To prevent silent misconfigurations, next build now intentionally fails if it detects a custom webpack function in next.config.ts (including one injected by a third-party plugin).
You have three ways to resolve a custom Webpack build failure:
- Migrate custom loaders or aliases to the top-level
turbopackconfiguration object. - Run
next build --turbopackto explicitly use Turbopack and ignore thewebpackblock. - Run
next build --webpackto opt out of Turbopack and continue bundling with Webpack.
Parallel routes, removed features, and updated defaults
Check four additional breaking changes before running a production build (Next.js 16 upgrade guide):
- Parallel routes (
@slot) requiredefault.tsx: Every parallel route slot must now include an explicitdefault.jsordefault.tsxfallback file, ornext buildfails. A minimal fallback can callnotFound()fromnext/navigationor returnnull. - Removals: The
next lintcommand (and automatic linting duringnext build),serverRuntimeConfig,publicRuntimeConfig, AMP support, and the legacyexperimental_pprflags have all been removed. Use the ESLint CLI or Biome directly in your CI scripts, and replace runtime configs with standard environment variables. - Image caching and security defaults: The default
images.minimumCacheTTLincreased from60seconds to14400seconds (4 hours) to lower revalidation CPU overhead on origin images withoutcache-controlheaders. Default image quality now caps at75,16px is removed fromimageSizes, and local IP image optimization is blocked by default. - Scroll behavior: Next.js no longer overrides
scroll-behavior: smoothduring route transitions unless you adddata-scroll-behavior="smooth"to the<html>element.
Here is a consolidated next.config.ts updated for Next.js 16.4.2, combining the top-level turbopack key, the new Cache Components and Partial Prefetching flags, and the experimental compiler improvements introduced in 16.4 (Next.js 16.4 release notes):
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
turbopack: {
resolveExtensions: ['.mdx', '.tsx', '.ts', '.jsx', '.js', '.json'],
},
experimental: {
turbopackGc: true,
turbopackRustReactCompiler: true,
},
images: {
minimumCacheTTL: 14400,
},
};
export default nextConfig;In Next.js 16.4, Turbopack reduces disk cache size by 20 to 25 percent and supports an experimental garbage collector (experimental.turbopackGc), while experimental.turbopackRustReactCompiler cuts memory usage by 30 percent and compile time by 15 percent by skipping files that do not require optimization and avoiding duplicate SSR work on Client Components (Next.js 16.4 release notes).
Adopting Next.js 16.4 navigation improvements and Partial Prefetching
Starting in Next.js 16.4, create-next-app enables both cacheComponents: true and partialPrefetching: true by default, and the Next.js team recommends adopting this model ahead of it becoming the framework default in Next.js 17 (Next.js 16.4 release notes).
As detailed in the optimizing prefetching guide and the v0 navigation case study, Partial Prefetching splits route loading into three distinct stages:
- App Shell prefetch (default
<Link>): Prefetches the shared static shell and session-specific UI that does not depend on target URL parameters. - Per-link prefetch (
<Link prefetch={true}>oruseRouter().prefetch()): Resolves the specific destination URL (such as/catalog/[id]) and prefetches static or cached content tied toparamsorsearchParams. - Navigation: Executes when the user clicks the link, streaming any remaining uncached or deferred subtrees.
Controlling stages with await prefetch() and await navigation()
When a component tree uses 'use cache', Next.js may include cached data in early prefetch stages. If a route has many visible links, prefetching every cached query can inflate server load. In the v0 case study, naive prefetching initially increased /templates/[id] prefetch requests by 2.9x before the team tuned stage boundaries.
Next.js 16.4 adds two functions in next/cache (prefetch() and navigation() API reference) to defer expensive cached subtrees without forcing them to be dynamic at request time:
await prefetch(): Excludes everything below it in the component subtree from the shared App Shell, running only during an explicit per-link prefetch (<Link prefetch={true}>) or full navigation.await navigation(): Excludes everything below it from both the App Shell and per-link prefetches. Prefetches receive the nearest<Suspense>fallback instead, and the subtree executes only when the user actually navigates.
Unlike connection(), neither function marks the subtree as request-dependent, so functions called after awaiting them can still use 'use cache':
// app/threads/[id]/page.tsx
import { Suspense } from 'react';
import { navigation, prefetch } from 'next/cache';
async function fetchThreadSummary(id: string) {
'use cache';
return { id, title: `Thread #${id}`, summary: 'Cached summary payload' };
}
async function fetchThreadReplies(id: string) {
'use cache';
return [
{ id: 'r1', body: 'First cached reply' },
{ id: 'r2', body: 'Second cached reply' },
];
}
async function ThreadHeader({ id }: { id: string }) {
await prefetch();
const summary = await fetchThreadSummary(id);
return (
<header>
<h2>{summary.title}</h2>
<p>{summary.summary}</p>
</header>
);
}
async function ThreadReplies({ id }: { id: string }) {
await navigation();
const replies = await fetchThreadReplies(id);
return (
<ul>
{replies.map((reply) => (
<li key={reply.id}>{reply.body}</li>
))}
</ul>
);
}
type ThreadPageProps = {
params: Promise<{ id: string }>;
};
export default async function ThreadPage(props: ThreadPageProps) {
const { id } = await props.params;
return (
<section>
<Suspense fallback={<p>Loading thread header...</p>}>
<ThreadHeader id={id} />
</Suspense>
<Suspense fallback={<p>Loading replies...</p>}>
<ThreadReplies id={id} />
</Suspense>
</section>
);
}To automate tuning these boundaries alongside Playwright's instant() test helper, you can also install the official agent skills introduced in 16.4 (npx skills add vercel/next.js --skill next-bundle-optimizer and next-partial-prefetching-optimizer) (Next.js 16.4 release notes).
Enforcing static output at build time with ensureStatic
If you maintain routes that must never fall back to request-time rendering (for example, high-traffic marketing layouts or static sites similar to shipping a static Next.js site on Firebase Hosting), Next.js 16.4 adds the ensureStatic Route Segment Config (ensureStatic API reference).
Exporting ensureStatic from a page or layout fails next build if any component in the route reads cookies(), headers(), connection(), or uncached data:
// app/docs/layout.tsx
import type { ReactNode } from 'react';
export const ensureStatic = 'navigation';
export default function DocsLayout({ children }: { children: ReactNode }) {
return <div className="docs-container">{children}</div>;
}Accepted values are 'auto' (default), 'shell' (only the App Shell must be static), 'prefetch' (App Shell and per-link prefetches must be static), 'navigation' (the entire server-rendered route must be static), or false.
Verifying the build and Turbopack fixes in v16.4.2
When pinning your target version, make sure your lockfile resolves to v16.4.2 (released October 15, 2026) rather than stopping at v16.4.0 or v16.4.1 (Next.js releases). While v16.4.1 delivered the October 14 security patches, v16.4.2 ships several important Turbopack stability fixes for production and hybrid routers:
- Fixes two garbage collection paths in Turbopack that previously left half-torn-down dependency edges (
#100209). - Resolves
turbo-taskssnapshot consistency bugs (#100002) and lazy dynamic-import chunk-list collisions (#100211). - Backports Pages Router development module graph chaining (
#100008) and ignoresrealpatherrors during module resolution to match standard Node.js behavior (#100217).
Run a clean typecheck and production build locally to verify that every dynamic API call, parallel route slot, and static constraint passes under Turbopack:
npx next typegen
npx tsc --noEmit
npx next buildOnce the build succeeds, check your build output table to confirm that routes marked with ensureStatic compile as static or partially prerendered shells, and inspect your lockfile one final time to confirm next@16.4.2 and sharp@0.35.5 (or newer) are locked in before the October 21, 2026 Next.js 15 end-of-life cutoff.
Sources
- Next.js 15 reaches end of life on 2026-10-21
- Next.js Security Update for Upstream Vulnerabilities
- nextjs.org
- September 2026 Security Release
- nextjs.org
- Next.js 16.4
- Improving navigation experience in v0 with Next.js 16.4
- nextjs.org
- nextjs.org
- nextjs.org
- github.com
- Upcoming Next.js Security Update for Upstream Vulnerabilities
- Next.js
- React
- TypeScript
- Security
- Migration
Related articles
Shipping a static Next.js site on Firebase Hosting
How this site is built: a Next.js static export served from Firebase Hosting, with URLs, caching and SEO metadata that agree with each other.
Full-Stack Development, 4 min read