Next.js 15 leaves its support window on 21 October 2026. From that date, the version most production Next.js apps still run on stops receiving the critical fixes and security updates it has been getting since Next.js 16 shipped a year ago.
Most upgrade guides stop at "run the codemod and fix the type errors." That gets you a green build. It does not get you a finished upgrade, because several of the changes in Next.js 16 compile cleanly and then quietly change what your users see: image quality, how long images stay cached, whether your CI still lints, and how the page scrolls between routes.
This guide is the checklist we work through on every Next.js upgrade. It is organised so you can run it top to bottom, and every section opens with the answer so you can skip to the parts that apply to your app. Version numbers are current as of 13 October 2026: Next.js 16.4.0 is the latest stable release, and the newest Next.js 15 backport is 15.5.27.
What happens to Next.js 15 on October 21, 2026
Next.js 15 reaches the end of its Maintenance LTS period on 21 October 2026, two years after its release on 21 October 2024. After that date it falls outside the Next.js support policy, which means no more scheduled critical bug fixes or security patches.
Your app does not stop working on the 22nd. Nothing is switched off. What changes is the risk profile: the next vulnerability disclosed in the framework will be fixed in Next.js 16, and a Next.js 15 app will either need that upgrade done under pressure or stay exposed. The policy does allow for patching unsupported versions "in rare circumstances" for severe issues, but that is an exception, not a plan.
The support model works like this. A major version stays in Active LTS, with features, fixes and security patches, until the next major ships. It then moves to Maintenance LTS, where it only receives critical bug fixes and essential security updates, for two years from its original release date. Next.js 16 has been Active LTS since 21 October 2025. Next.js 15 has been in maintenance since the same day, and its clock runs out next week.
If you are still on Next.js 14, you are already a year past its end of life, which was 26 October 2025. The same upgrade path applies, with the Next.js 15 breaking changes stacked on top.
Why a green build is not a finished upgrade
A passing next build proves your code compiles under the new rules. It says nothing about whether the app behaves the way it did before, and in Next.js 16 several changes are deliberately silent.
The official upgrade guide says as much in its own migration checklist: a green build alone does not close runtime findings. Here are the changes that fit that description, each covered in detail below:
Image requests for a quality other than 75 are now quietly coerced to the nearest allowed value, so images can look different without any error in the build.
Optimised images without cache headers now stay cached for four hours instead of sixty seconds, so an image replaced in your CMS can keep serving the old version all afternoon.
next buildno longer runs ESLint, so a pipeline that relied on the build to catch lint errors now ships them.Next.js no longer overrides a global
scroll-behavior: smoothduring route changes, so navigations that used to jump to the top now animate.A sitemap
idis now a promise of a string, and plain JavaScript will not warn you when arithmetic on it produces the wrong number.
Each of these is a reasonable default on its own. Together they are why the testing section near the end of this guide matters more than the codemod at the start.
Check Node.js, TypeScript and browser requirements first
Next.js 16 requires Node.js 20.9.0 or newer, TypeScript 5.1.0 or newer, and drops support for older browsers. Check these before touching any code, because a mismatch here produces confusing errors later.
Requirement | Next.js 16 minimum |
|---|---|
Node.js | 20.9.0 (Node.js 18 is no longer supported) |
TypeScript | 5.1.0 |
Browsers | Chrome 111+, Edge 111+, Firefox 111+, Safari 16.4+ |
Three places tend to get missed. The first is the Node.js version in your hosting configuration, which is separate from the one on your laptop. The second is the Node.js image in your CI workflow and any Dockerfile. The third is your browser support promise: if a client contract or analytics data says you support older Safari versions, that conversation needs to happen before the upgrade, not after.
Update @types/react and @types/react-dom alongside react and react-dom. The App Router in Next.js 16 runs on the React canary channel that includes React 19.2 features such as View Transitions, useEffectEvent and Activity, and stale type packages produce misleading errors.
Run the upgrade codemod, then run the one it skips
Start on a dedicated branch and run the official upgrade codemod:
npx @next/codemod@canary upgrade latestAccording to the codemod documentation, this updates next.config.js for the new top-level turbopack option, migrates next lint to the ESLint CLI, renames the middleware convention to proxy, removes the unstable_ prefix from stabilised APIs, and removes the experimental_ppr route segment config.
What it does not do is migrate synchronous request APIs. If your app still reads params, searchParams, cookies(), headers() or draftMode() synchronously, which Next.js 15 allowed with a warning during its compatibility period, run the second codemod as well:
npx @next/codemod@canary next-async-request-api .This is the single most common reason an upgrade that "ran the codemod" still fails, so it is worth running even if you believe your code is already async.
If you work with a coding agent, the guide now recommends npx next@canary upgrade --agent=latest, which prepares the migration guides and codemod commands for the agent to apply and verify. The checklist below still applies, because an agent can also produce a green build that behaves differently.
Async request APIs: synchronous access is gone
In Next.js 16, cookies, headers, draftMode, params and searchParams can only be read asynchronously. Next.js 15 introduced the async versions with a temporary synchronous fallback, and that fallback has been removed.
The change covers params in layout, page, route and default files, in opengraph-image, twitter-image, icon and apple-icon, and searchParams in page files.
The cleanest way to get the types right is the generated helpers. Run npx next typegen and you get globally available PageProps, LayoutProps and RouteContext types that know your route parameters:
// app/blog/[slug]/page.tsx
export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
const query = await props.searchParams
return <h1>Blog Post: {slug}</h1>
}After the codemod runs, search the codebase for UnsafeUnwrapped and for comments the codemod left where it could not migrate safely. Those markers are easy to commit by accident and they are exactly the places the upgrade guide tells you to resolve. In Client Components that need a value synchronously, React's use hook is the documented route.
Metadata images and sitemaps now receive promises
The image generation functions behind opengraph-image, twitter-image, icon and apple-icon now receive params and id as promises, and so does the sitemap function when you use generateSitemaps. The generateImageMetadata and generateSitemaps functions themselves still receive synchronous values.
For Open Graph images, the fix is mechanical:
// app/shop/[slug]/opengraph-image.js
export default async function Image({ params, id }) {
const { slug } = await params
const imageId = await id
// ...
}Sitemaps deserve more care. In Next.js 15 the id passed to sitemap was the number you returned from generateSitemaps. In Next.js 16 it is a promise that resolves to a string. TypeScript will flag the missing await, but in a plain JavaScript project nothing stops id * 50000 from quietly evaluating to NaN, and nothing stops resolvedId + 1 from producing "11" instead of 2. Either one gives you a sitemap that builds, deploys and lists the wrong URLs.
// app/product/sitemap.js
export default async function sitemap({ id }) {
const resolvedId = await id
const start = Number(resolvedId) * 50000
// ...
}Convert to a number explicitly, then open the generated sitemap files in a browser and check the first and last URL in each one. Sitemaps are not something anyone looks at after launch, which is why a broken one can go unnoticed until Search Console starts reporting coverage drops.
Turbopack is the default bundler, and custom webpack config fails the build
Turbopack is now stable and used by default for both next dev and next build. If your project has a custom webpack configuration, next build will fail on purpose rather than silently ignore it.
You can remove the --turbopack and --turbo flags from your scripts. If the build fails because a webpack config was found, you have three options:
Migrate the configuration to Turbopack equivalents, which is the long-term answer.
Keep webpack for production builds with
next build --webpackwhile you migrate.Build with Turbopack and ignore the webpack config, but only if you are sure nothing in it matters.
If you never wrote a webpack config yourself and still see this error, a plugin added one. Bundle analysers, MDX setups, SVG loaders and some monitoring SDKs are common sources.
The experimental.turbopack block has moved to a top-level turbopack key in next.config.ts. Two smaller changes catch people out. Turbopack does not support the legacy tilde prefix in Sass imports, so @import '~bootstrap/...' becomes @import 'bootstrap/...'. And code that relied on webpack's resolve.fallback to silence errors about Node.js modules like fs in client bundles needs turbopack.resolveAlias instead, although the better fix is to stop importing server modules from client code.
middleware.ts becomes proxy.ts, unless you run on the edge
The middleware file convention is deprecated and renamed to proxy, along with the exported function name and any config flags containing the word, such as skipMiddlewareUrlNormalize, which becomes skipProxyUrlNormalize.
mv middleware.ts proxy.ts// proxy.ts
export function proxy(request: Request) {}The important detail is the runtime. proxy runs on Node.js, and that cannot be configured. The edge runtime is not supported in proxy. If your middleware currently runs on the edge, for geolocation, low-latency redirects or A/B routing at the network edge, the guidance is to keep using middleware for now rather than renaming it.
The upgrade codemod handles the rename. After it runs, check whether any of your middleware declared the edge runtime, and if it did, decide deliberately whether to move it to Node.js or keep the old convention. The proxy documentation is the reference to read before choosing.
next/image defaults changed in six ways
next/image has six breaking changes in Next.js 16, and most of them change behaviour without producing an error. This is the section where we spend the most testing time.
Quality values. images.qualities now defaults to [75] only. A quality={90} prop is quietly coerced to the closest allowed value, so it renders at 75. Direct requests to /_next/image with an unsupported q value return a 400. If your design depends on higher-quality hero or product images, list the values you use:
images: {
qualities: [50, 75, 90],
}Cache lifetime. images.minimumCacheTTL now defaults to 4 hours (14,400 seconds) instead of 60 seconds. This reduces CPU cost when upstream images lack cache headers, which is a sensible default. The side effect is that an image replaced at the same URL in a headless CMS can keep serving the old version for hours. If editors expect instant changes, either set a lower value or change the image URL on update.
Image sizes. 16 has been removed from the default images.imageSizes array, which shortens the srcset. Add it back only if you genuinely serve 16-pixel images.
Local images with query strings. A local source like /assets/photo?v=1 now requires a matching images.localPatterns entry with a search value. This prevents enumeration attacks, and it also breaks cache-busting query strings that used to work.
Local IP addresses. Optimising images from local IP addresses is now blocked by default. Hosting inside a VPC with split-horizon DNS is the usual way to hit this, and it shows up as a 400. images.dangerouslyAllowLocalIP exists for private networks, and the name tells you how carefully to use it.
Redirects. images.maximumRedirects now defaults to 3 rather than unlimited. Image CDNs that chain redirects can start failing.
Two deprecations belong on the same list. next/legacy/image is deprecated in favour of next/image, and images.domains is deprecated in favour of images.remotePatterns, which is stricter about protocol and hostname. The full option list is in the image configuration reference.
Caching APIs: revalidateTag needs a profile, and updateTag is new
revalidateTag now takes a second argument naming a cacheLife profile. The single-argument form is deprecated and produces a TypeScript error.
// Before
revalidateTag('posts')
// After
revalidateTag('posts', 'max')The more interesting change is what the second argument forces you to decide. revalidateTag gives stale-while-revalidate behaviour: readers see the old content while fresh data loads in the background. That is right for blog posts, product catalogues and documentation.
For changes a user makes and expects to see immediately, Next.js 16 adds updateTag, which is only available in Server Actions and gives read-your-writes semantics by expiring and refreshing the data 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)
updateTag(`user-${userId}`)
}There is also a new refresh() for refreshing the client router from inside a Server Action, which suits cases like updating a notification count after marking something as read. cacheLife and cacheTag are stable, so imports aliased from unstable_cacheLife and unstable_cacheTag can be simplified.
When you review each revalidateTag call during the upgrade, ask one question: would a user who just made this change be confused to see the old value? If yes, it should probably be updateTag.
Parallel route slots now need default.js
Every parallel route slot now requires an explicit default.js file, and builds fail without one. This one is loud, which makes it easy.
To keep the previous behaviour, add a default.js that either calls notFound() or returns null:
// app/@modal/default.tsx
export default function Default() {
return null
}The decision between the two matters. Returning null renders nothing in that slot on a hard navigation or reload. Calling notFound() turns the whole route into a 404. Test both a direct visit and a browser refresh on any route that uses a modal or a slot, because that is where the difference shows. The default.js reference explains when each one fires.
next lint is gone, and next build no longer lints
The next lint command has been removed, next build no longer runs linting, and the eslint option in next.config is no longer supported. If your CI relied on the build step to catch lint errors, it has stopped doing that.
The codemod migrates your scripts to the ESLint CLI:
npx @next/codemod@canary next-lint-to-eslint-cli .Then add an explicit lint step to your pipeline before the build, so a lint failure blocks a merge the way it used to. Biome is an equally valid choice if you were already considering it.
@next/eslint-plugin-next now defaults to the ESLint flat config format, in line with ESLint 10 dropping legacy configuration. If you still use .eslintrc, the ESLint configuration reference covers the flat config setup. One practical note from the official troubleshooting section: migrating off next lint does not require upgrading ESLint itself. eslint-config-next@16 needs ESLint 9, but if an ESLint upgrade causes peer dependency conflicts, you can restore your previous lint stack and run the upgrade with --skip-eslint-upgrade.
Removed features to search your codebase for
Several features deprecated in earlier versions are now removed outright. A quick search for each one tells you whether any of these sections apply to you.
AMP. All AMP APIs are gone, including the amp config, useAmp from next/amp, and export const config = { amp: true }. If you still serve AMP pages, this is a product decision before it is a code change.
Runtime configuration. serverRuntimeConfig and publicRuntimeConfig have been removed. Server-only values come from process.env directly in Server Components. Values the browser needs use the NEXT_PUBLIC_ prefix. Note that NEXT_PUBLIC_ values are inlined at build time, so if you relied on runtime config to change a value without rebuilding, call await connection() from next/server before reading process.env, which forces the read to happen at request time.
Experimental flags. experimental.dynamicIO, experimental.useCache and the experimental Partial Prerendering flag are gone. If you were actively using them, the replacement is the top-level cacheComponents option. If you were not, delete them rather than renaming them, because enabling cacheComponents is a model change, not a rename.
Smaller removals. unstable_rootParams is replaced by next/root-params, and the appIsrStatus, buildActivity and buildActivityPosition options under devIndicators are removed, although the indicator itself remains.
Search terms worth running: next/amp, RuntimeConfig, getConfig, dynamicIO, useCache, experimental_ppr, rootParams and devIndicators.
Smaller behaviour changes that surprise people
A handful of changes do not break anything but will generate questions from your team or your clients. Knowing about them in advance saves time.
Smooth scrolling between routes. If your global CSS sets scroll-behavior: smooth on <html>, earlier versions temporarily overrode it during navigation so route changes snapped to the top. Next.js 16 no longer does this, so navigations now animate. To restore the old behaviour, add data-scroll-behavior="smooth" to the <html> element in your root layout.
Build output metrics. The size and First Load JS columns are gone from next build output because they were inaccurate with React Server Components. If someone on your team tracked bundle size from that output, switch to Lighthouse or real-user Core Web Vitals data. Our guide to passing Core Web Vitals on real phones covers how to measure what users actually experience.
Separate dev output. next dev now writes to .next/dev, so dev and build can run at the same time, and a lockfile stops two instances of the same command from running on one project. Scripts that cleaned or inspected .next directly may need a path update.
Config loaded once in development. The config file is no longer loaded twice during next dev, which means checking whether process.argv includes 'dev' inside next.config now returns false. Plugins that used that check to start side processes should check process.env.NODE_ENV === 'development' or use the config phase instead.
Prefetching. Navigation prefetching has been rebuilt with layout deduplication and incremental prefetching. You may see more individual prefetch requests in the network tab, each much smaller in total transfer size. This is expected and needs no code changes.
A testing plan that catches what the build misses
The build tells you the code compiles. This testing plan tells you the app still behaves the same way. We run it on a preview deployment of the upgrade branch, side by side with production.
Images. Open your heaviest image pages, typically the homepage, a product or portfolio page and a blog post, on both deployments. Compare visually at full size. Check the network tab for any
/_next/imagerequest returning 400.Content freshness. Replace an image and edit a piece of content in your CMS. Confirm both update within the time your editors expect.
Sitemaps and metadata. Open every generated sitemap and check the first and last URLs. Share a page link in a social preview tool to confirm Open Graph images render.
Routes with slots or modals. Visit each one directly, then refresh the browser on it.
Proxy or middleware. Exercise every redirect, rewrite, auth gate and locale rule it handles. If you changed runtimes, measure latency from more than one region.
Forms and Server Actions. Submit every form that changes data and confirm the user sees their change immediately where they should.
Navigation feel. Click through the main routes and confirm scroll behaviour matches what you intend.
CI. Push a commit with a deliberate lint error and confirm the pipeline fails.
Real-user metrics. After release, watch Core Web Vitals and server CPU for a few days. The image cache change usually lowers CPU, and a sudden rise is worth investigating.
If you'd rather work from paper, the printable upgrade worksheet has this test plan as tick boxes with a pass condition for each one.
The upgrade guide closes with its own migration checklist, and it is worth reading line by line before you merge. One line in particular is worth repeating to anyone in a hurry: do not enable React Compiler or Cache Components just to complete the upgrade.
Should you turn on React Compiler or Cache Components during the upgrade?
No. Both are worth evaluating, and neither belongs in the same pull request as a major version upgrade.
React Compiler support is stable in Next.js 16 through the top-level reactCompiler option, following React Compiler 1.0. It is not enabled by default, it requires babel-plugin-react-compiler, and the documentation notes that it increases compile times because it relies on Babel. It is a reasonable experiment once the upgrade has settled, measured against real render performance.
Cache Components, enabled through cacheComponents: true, is how Partial Prerendering works in Next.js 16. It can surface build errors for uncached data accessed outside a <Suspense> boundary and asks you to adopt a different caching model. The Cache Components migration guide is the place to start when you are ready.
Keeping these separate is basic change management. If something regresses after release, you want one change to suspect, not three. For teams building larger products, our post on multi-tenant SaaS architecture covers the same principle at the system level: isolate changes so failures stay small and traceable.
The short version
Next.js 15 support ends on 21 October 2026. Run both codemods, fix what the build reports, then spend real time on the changes it cannot report: image quality and caching, sitemap IDs, CI linting, scroll behaviour and any edge middleware. Keep React Compiler and Cache Components for a later, separate change.
If you are planning the rest of your stack for the coming year, our roundup of what actually changed in web development in 2026 puts this release in context. The full detail for everything above lives in the official Next.js 16 upgrade guide, which is worth keeping open in a tab while you work.