Type something to search...
Next.js Upgrading

Next.js Upgrading

Next.js moves fast. The team ships canary builds almost daily, minor releases every few weeks, and a major version roughly once or twice a year. That pace is one of the reasons the framework stays ahead of the React ecosystem — Server Components, Turbopack, and the caching model have all matured in public, in small increments, rather than arriving as one disruptive rewrite. But it also means an app you haven't touched in six months is probably several minor versions and at least one major version behind, and "just bump the version number" is rarely the whole story.

Upgrading a Next.js application is its own skill, distinct from upgrading a typical npm dependency. You're not just pulling in bug fixes — you're often changing the version of React underneath your app, picking up new default behaviors in the bundler, and sometimes inheriting renamed file conventions that the framework will silently stop recognizing if you don't rename them yourself. This article covers how the upgrade command and canary channel actually work, what the version guides are for, and the workflow I'd recommend for doing this without breaking your app in production.

Why Upgrading Next.js Isn't Like Upgrading a Regular Package

When you upgrade most libraries, you bump one line in package.json, reinstall, and move on. Next.js doesn't work that way for two structural reasons.

First, Next.js and React are versioned together. The App Router relies on React features that are still being finalized upstream — Server Components, useEffectEvent, the Activity component, View Transitions — which means each Next.js release is built and tested against a specific React release, often a canary build of React itself rather than a stable one. If you bump next without also bumping react and react-dom to the versions it expects, you can end up with subtle rendering bugs, hook mismatches, or hydration errors that have nothing to do with your own code. This is why every upgrade command and every manual install instruction you'll see bumps all three packages together, never just next on its own.

Second, a meaningful share of Next.js's improvements land as changed defaults, not new APIs. Turbopack becoming the default bundler in a given release, a shorter or longer default cache TTL for images, a renamed configuration file — none of these require you to write a single line of new code, but all of them can change how your existing code behaves the moment you upgrade. You can't "not adopt" a changed default the way you can skip a new opt-in feature. This is the main reason version guides exist as a separate category of documentation: they're not release notes, they're a list of things that will silently affect you whether you asked for them or not.

Keeping those two points in mind reframes the whole upgrade process. It's not "install the new version," it's "install the new version, then go find out what changed underneath you."

The Two Ways to Get to the Latest Stable Release

Since Next.js 16.1, there's a first-party command that handles this for you:

# npm
npx next upgrade

# pnpm
pnpm next upgrade

# yarn
yarn next upgrade

# bun
bunx next upgrade

This isn't a thin wrapper around npm install next@latest. Under the hood, next upgrade bumps next, react, react-dom, and eslint-config-next together to compatible versions, and it's aware of which codemods apply to the jump you're making, so in many cases it applies the relevant automated migrations as part of the same command. That distinction matters: running the upgrade command and running a manual package bump are not equivalent operations, even though they can land you on the same version numbers.

If your project predates Next.js 16.1, that command doesn't exist yet in your installed CLI, so you need a separate package to get the same effect:

npx @next/codemod@canary upgrade latest

And if you'd rather skip any automated tooling entirely and do it by hand, a plain manual install still works:

# npm
npm i next@latest react@latest react-dom@latest eslint-config-next@latest

# yarn
yarn add next@latest react@latest react-dom@latest eslint-config-next@latest

# pnpm
pnpm i next@latest react@latest react-dom@latest eslint-config-next@latest

# bun
bun add next@latest react@latest react-dom@latest eslint-config-next@latest

Notice eslint-config-next is bumped alongside the other three in every one of these commands. It's easy to forget this one because it's a devDependency and doesn't affect runtime behavior, but an outdated ESLint config for Next.js will happily let you write code patterns the new version has deprecated, so you find out about them at build time instead of while you're typing. Bump it every time, not just when you notice lint rules feel stale.

If you're on TypeScript, there's a fourth thing worth doing manually that none of these commands touch for you: bump @types/react and @types/react-dom too. React's type definitions are published separately from React itself, and a mismatch between your installed React version and its types package is a classic source of type errors that have nothing to do with your actual code.

Moving to the Canary Channel

Stable releases are conservative by design — a feature generally sits in canary for weeks or months, proving itself against the framework's own test suite and real usage from people willing to take on some risk, before it's promoted to a stable release. If you want early access to a fix that's already merged but not yet released, or a feature that's still stabilizing, canary is how you get it:

# npm
npm i next@canary

# pnpm
pnpm add next@canary

# yarn
yarn add next@canary

# bun
bun add next@canary

The official guidance is to only move to canary once you're already on the latest stable release and confident your app works correctly there. That ordering isn't a formality — canary is a moving target that changes daily, and if you jump to it from an old stable version, you have no way to tell whether a bug you hit is a new canary regression or something that was already broken two stable releases ago. Upgrade to latest stable first, verify your app, then move to canary if you still need to.

It's worth being honest with your team about what "canary" means before adopting it anywhere near production. It's not a beta channel with a fixed feature set — it's the tip of the framework's own development branch. Features can change shape between one canary build and the next, and something that works today can behave differently in tomorrow's build without any changelog entry pointing you to why. Canary is genuinely useful for feature previews, for testing whether a bug you reported has been fixed yet, and for teams actively co-developing with the framework, but it's a poor choice for the one production app that nobody has time to babysit.

Features That Ship in Canary Before They're Stable

The canary channel isn't only for bug fixes — it's also where entire features live during their stabilization period. As of writing, a cluster of authentication-related APIs sit in canary ahead of general availability:

  • forbidden() and unauthorized() — functions you call to intentionally render a 403 or 401 response from within a Server Component, Server Action, or Route Handler
  • forbidden.js and unauthorized.js — the file conventions that let you define custom UI for those states, the same way not-found.js lets you customize a 404 page
  • authInterrupts — the configuration flag you need to set before any of the above will work at all

If you've read a tutorial or a Stack Overflow answer that calls forbidden() directly and it throws an error that the function doesn't exist, this is almost always why — you're on a stable release where the feature hasn't landed yet, or you're on canary but haven't enabled authInterrupts. Checking the canary feature list before you assume your setup is broken will save you a confusing afternoon.

Version Guides: The Part Most Teams Skip

Every major version bump has a dedicated guide — "How to upgrade to version 14," "version 15," "version 16," and so on. These aren't changelogs. A changelog tells you everything that shipped; a version guide tells you specifically what you need to do to your codebase because of it, ordered as an actual migration checklist. Skipping this document and just running the automated upgrade tooling is the single most common way teams end up debugging a production incident that traces back to a change they never read about.

To make this concrete, here's a sample of what the Next.js 16 guide actually covers, not as an exhaustive list, but as a demonstration of the kind of thing these guides catch that a simple version bump won't:

Turbopack becomes the default bundler. Running next dev or next build with no flags now uses Turbopack instead of Webpack. If your project has a custom Webpack configuration, next build will fail on purpose rather than silently ignore your config — a safety net so you don't accidentally ship a build that's missing configuration you were relying on. You get three ways out: force the build to use Turbopack anyway with --turbopack and ignore the Webpack config, migrate the config to Turbopack's equivalent options, or explicitly opt back into Webpack with --webpack.

Async Request APIs are no longer just deprecated, they're gone. cookies(), headers(), draftMode(), and the params/searchParams props on pages became asynchronous starting in Next.js 15, but the framework kept a temporary synchronous compatibility path so existing code wouldn't break immediately. In version 16, that compatibility path is fully removed. If your code still does const { slug } = params instead of const { slug } = await params, it will throw at runtime, not just log a warning.

middleware is renamed to proxy. The filename, the exported function name, and any configuration flags containing the word "middleware" (like skipMiddlewareUrlNormalize) are all renamed to their proxy equivalents, to better reflect what the file actually does. The Edge runtime is explicitly not supported for proxy — if you need Edge, you stay on middleware for now.

next/image default behavior changes in several small ways at once — the cache TTL for unheadered images jumps from 60 seconds to 4 hours, the smallest generated image size (16px) is dropped from the default imageSizes array, and the default quality list narrows to [75] only. None of these require code changes, but all of them can change what your existing <Image> usage actually serves to users, and they're the kind of thing that's very easy to attribute to "a CDN issue" instead of "we upgraded Next.js" if you don't know to look for it.

next lint is removed entirely, in favor of running ESLint (or Biome) directly, and a codemod exists specifically to migrate your next lint invocation to a plain ESLint CLI command.

That's a fraction of one version guide, and every one of those six items is the kind of thing that either breaks your build, silently changes user-facing behavior, or throws a runtime error you'll only discover once real traffic hits the affected code path. This is exactly why "read the version guide for the version you're jumping to" belongs at the top of any upgrade checklist, not as an optional last step.

Running the Codemod

For the mechanical, repetitive parts of a migration — renaming a file, adding an await in front of a known set of function calls, updating a config key — Next.js ships codemods so you don't have to do it by hand across dozens of files. The general entry point looks like this:

npx @next/codemod@canary upgrade latest

This single command is capable of handling several unrelated migrations in one pass: updating next.config.js to use the newer top-level turbopack key instead of the old experimental.turbopack nesting, migrating a next lint script to a direct ESLint CLI invocation, renaming a middleware file and its exports to proxy, stripping the now-unnecessary unstable_ prefix off APIs that have since stabilized (unstable_cacheLife becoming cacheLife, for instance), and removing an experimental route segment config flag that's no longer read.

What it deliberately does not do is run every possible migration for you. If your project still uses the temporary synchronous form of cookies(), headers(), params, or searchParams left over from the version 15 compatibility window, that's a separate, more invasive codemod you need to run on purpose:

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

The reason it's separate is that this particular migration touches business logic, not just boilerplate — it has to insert await in the right places across your components, which is a bigger, riskier change than a mechanical rename. Running it deliberately, on its own commit, means you can review that diff in isolation instead of it getting lost inside a larger "upgrade Next.js" commit.

Whichever codemod you run, treat the output as a first draft, not a finished migration. Codemods are pattern-based rewrites — they're excellent at bulk mechanical changes and genuinely bad at anything that requires understanding what your code is trying to do. Always diff the result before committing it.

Letting an AI Coding Agent Drive the Upgrade

Next.js's own documentation now leans into this directly: the version guides include a ready-made prompt meant to be handed to an AI coding agent, on the theory that the mechanical parts of an upgrade — running the codemod, fixing the follow-up breakages it doesn't catch, verifying the app still runs — are exactly the kind of repetitive, checkable work an agent handles well, as long as it's reading version-matched documentation instead of guessing from stale training data.

The mechanism that makes this reliable is an AGENTS.md file at your project root. Recent versions of Next.js can generate and maintain a managed block inside it automatically:

npx @next/codemod@canary agents-md

That block exists to solve a specific, easy-to-miss problem: a coding agent's training data has a cutoff date, and Next.js changes fast enough that an agent's default assumptions about file conventions, APIs, and config keys can simply be wrong for the version actually installed in your project. The generated block tells the agent, in effect, "don't trust what you already know — go read the docs bundled inside node_modules/next/dist/docs/ for this exact version before writing anything." If you've ever wondered why a fresh Next.js project sometimes ships with a block like that already committed, this is why — and it's worth keeping rather than deleting, since removing it from a diff only causes the framework to regenerate it the next time you run next dev.

If you want to try this approach yourself, the shape of the prompt is roughly: point the agent at the version guide for your target release as the source of truth, tell it to use the codemod for the mechanical changes, ask it to explain its plan before making sweeping edits, and have it run your build and dev server afterward to verify nothing regressed — checking both the terminal output and, where available, the browser console. Treat the agent the way you'd treat a codemod: a strong first pass that still needs a human reviewing the diff before it merges, not a replacement for actually reading the version guide yourself.

A Safe Upgrade Workflow

Here's the sequence I use, roughly in order, whether I'm bumping one minor version or jumping across a major release:

  1. Read the version guide first, even if you're only skimming it. You specifically want the breaking-change sections and anything marked as a changed default — those are the things that won't show up as a compile error.
  2. Do the upgrade on its own branch, isolated from any feature work. An upgrade that touches your bundler, your caching layer, and possibly your middleware file has no business sharing a diff with an unrelated feature.
  3. Run the upgrade command or codemod, then immediately diff every file it touched. Don't skim it — actually read the diff, especially anywhere the codemod added await, renamed an export, or changed a config value.
  4. Check your next.config.js against the version guide's config-related changes. Renamed keys, removed experimental flags, and new required options tend to hide here, and this file rarely has good test coverage of its own.
  5. Run a full local build, not just next dev. Turbopack's dev server and next build catch different classes of problems — a custom Webpack config failure, for example, is a build-time error, not something dev mode will ever surface.
  6. Run your existing test suite and manually exercise anything the version guide flagged, particularly async data APIs, image components, and anything touching your renamed middleware/proxy file.
  7. Deploy to a staging environment before production, and specifically look at server logs for new warnings, not just outright errors. Deprecation warnings today are removed APIs in the next major version, and they're cheap to fix while they're still just warnings.
  8. Bump one version (or one guide) at a time when you're several releases behind. Jumping from Next.js 13 straight to 16 compounds every breaking change from three major versions into a single unreviewable diff. Upgrading through each major version guide in sequence, committing and testing at each stop, is slower but dramatically easier to debug when something goes wrong.

That last point is the one people skip most often, usually because it feels slower. It isn't, in practice — the total time spent debugging one giant multi-version leap almost always exceeds the time spent doing three smaller, sequential ones.

Common Gotchas

A few mistakes come up often enough to call out directly:

Bumping next without bumping react and react-dom to matching versions. This is the single most common source of "upgraded and now everything is broken" reports, and the fix is almost always just running the proper upgrade command instead of hand-editing one line in package.json.

Forgetting @types/react and @types/react-dom on a TypeScript project. You'll see type errors that look like they're about your components, when they're actually about a mismatch between your installed React version and its stale type definitions.

Assuming a custom Webpack config will keep working under Turbopack by default. As of version 16, it won't — next build fails intentionally rather than silently dropping half your configuration. If you see a build failure mentioning a Webpack config after upgrading, that's this, not a bug.

Treating canary as a long-term production channel. It's a fantastic way to verify a fix or preview a feature; it's a poor foundation for an app nobody is actively watching for regressions.

Running the automated codemod and assuming it caught everything. Codemods handle the mechanical, well-defined migrations. Anything context-dependent — a data-fetching pattern the framework doesn't recognize, a third-party library that assumed the old synchronous APIs — is still your job to find and fix by hand.

Running next dev and next build at the same time and being confused by the result. Newer versions of Next.js write dev and build output to separate directories and use a lockfile to prevent two instances from clobbering each other's output, precisely because this used to produce confusing, hard-to-reproduce errors. If you hit a lock-related error after upgrading, it usually means a leftover process from before the upgrade is still holding the lock — kill it and try again, rather than assuming your project is broken.

Rolling Back When an Upgrade Doesn't Go Cleanly

Sometimes the least glamorous move is the correct one: revert. If you've followed the workflow above and done the upgrade on its own branch, rolling back is just reverting package.json, your lockfile, and whatever config or codemod changes landed alongside them, then reinstalling. This is precisely why the upgrade deserves its own branch and its own commit, separate from feature work — a clean revert only stays clean if the upgrade never got tangled up with unrelated changes in the first place.

If you're mid-upgrade and something is broken but you're not sure whether it's the new Next.js version or your own code, the fastest way to isolate it is usually to check out a fresh clone of the same commit in a separate directory, downgrade only next, react, and react-dom back to their previous versions there, and see if the problem disappears. If it does, you've confirmed the regression is upgrade-related and you can report it or dig into the specific version guide section that's most likely responsible, instead of debugging your own application code for an issue that isn't there.

Should You Stay Current or Lag One Version Behind?

There's no universally correct answer here, but a reasonable default is: track the latest stable release fairly closely — within a minor version or two — but don't treat a brand-new major release as something to adopt on day one. Major versions tend to receive a handful of patch releases in their first few weeks that fix edge cases the release itself introduced. Waiting two to four weeks after a major release, then upgrading deliberately using the version guide, tends to be less eventful than upgrading the same day it ships. Canary, meanwhile, is worth adopting selectively and temporarily — to verify a specific fix, or to preview a feature you're evaluating — rather than as your team's permanent default.

Key Takeaways

ScenarioWhat to run
Next.js 16.1+, upgrade to latest stablenext upgrade
Older than 16.1, upgrade to latest stablenpx @next/codemod@canary upgrade latest
Manual install, no automated migrationsnpm i next@latest react@latest react-dom@latest eslint-config-next@latest
Want early access to unreleased fixes/featuresnpm i next@canary (only after you're already on latest stable)
Migrating off the old synchronous Request APIsnpx @next/codemod@canary next-async-request-api .
Understanding what will actually breakRead the version guide for your target release, not just the changelog

Upgrading Next.js is routine work, but it's not zero-risk work, and the framework's release cadence means it's a recurring task rather than a one-time event. The teams that handle it smoothly aren't the ones with some special tooling — they're the ones who've made "read the version guide, upgrade on a branch, diff the codemod output, test before deploying" a boring, repeatable habit instead of something they figure out fresh under pressure every time a major version ships.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper