Next.js 16 migration: async request APIs, Cache Components and proxy.ts, with code

Sync cookies() and params are gone. A custom webpack config now fails your build outright.

Read time
14 min
Word count
2.1K
Sections
14
FAQs
8
Share
Isometric illustration of a code migration pipeline turning synchronous blocks into async components
Next.js 16 removes synchronous cookies(), headers() and params entirely; the codemod does most of it.
On this page · 14 sections
  1. What will break, ranked by how fast it stops you
  2. Start with the codemod, then read what it missed
  3. Async request APIs: the change that touches every route
  4. Turbopack by default, and the build that fails on purpose
  5. Cache Components, and why caching is now opt-in
  6. The caching API changes that produce type errors
  7. middleware.ts becomes proxy.ts, and loses the Edge runtime
  8. The next/image defaults nobody reads until something looks wrong
  9. Removals and quiet behaviour changes
  10. India-specific considerations
  11. What we would sequence, and in what order
  12. FAQ
  13. How eCorpIT can help
  14. References

Summary. Next.js 16 shipped on 21 October 2025, and the documentation now tracks 16.2.10 as of 13 May 2026. It is the release that stops being polite about deprecations. Synchronous access to cookies(), headers(), draftMode(), params and searchParams is fully removed, having been merely deprecated in Next.js 15. Turbopack becomes the default bundler for both next dev and next build, and if you have a custom webpack configuration your production build will fail outright rather than warn. middleware.ts is renamed to proxy.ts, and the Edge runtime is not supported in proxy. Node.js 18 is dropped, with 20.9.0 the new minimum, alongside TypeScript 5.1 and Chrome, Edge and Firefox 111 or Safari 16.4. Four next/image defaults changed, next lint and AMP are gone, and every parallel route slot now needs an explicit default.js or the build fails. The images.imageSizes default drops the 16px entry, which Vercel says only 4.2% of projects used. Vercel reports Turbopack delivering 2 to 5 times faster production builds and up to 10 times faster Fast Refresh, and said more than 50% of development sessions and 20% of production builds on Next.js 15.3 and later were already running on Turbopack before the release. That speed is the payment for the work below.

What will break, ranked by how fast it stops you

Most upgrade guides list changes alphabetically. That is not the order you hit them in. This is:

Breaking change Fails at Codemod? Effort
Custom webpack config with next build Build, immediately No, use --webpack flag Minutes to decide, weeks to migrate
Parallel route slots missing default.js Build No Minutes per slot
Sync cookies(), headers(), params, searchParams Type check and runtime Yes, next-async-request-api Hours, mostly automated
middleware.ts still named that Deprecation, then removal later Yes, the v16 upgrade codemod Minutes
revalidateTag() with one argument TypeScript error No Hours, needs judgement
Node.js 18 runtime Install or CI No Depends on your platform

Work top-down. The first two stop the build with no output. The third produces a wall of type errors that looks catastrophic and is mostly mechanical.

Start with the codemod, then read what it missed

Vercel ships an automated upgrade path, and it does more than bump a version number. From the official version 16 upgrade guide:


            # Automated upgrade
npx @next/codemod@canary upgrade latest

# Or manually
npm install next@latest react@latest react-dom@latest
          

The v16 codemod updates next.config.js to the new turbopack configuration, migrates next lint to the ESLint CLI, migrates the deprecated middleware convention to proxy, removes the unstable_ prefix from stabilised APIs, and removes the experimental_ppr route segment config. That covers a genuine majority of the mechanical work.

For the async request APIs specifically, run the dedicated codemod:


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

Where an automatic migration is not possible, the codemod adds a typecast in TypeScript files or a comment, and leaves it for you. Those comments are the actual work. Do not merge the codemod's output without reading them.

Async request APIs: the change that touches every route

Next.js 15 introduced async request APIs with temporary synchronous compatibility. Next.js 16 removes that compatibility entirely. The affected surface is wide: cookies, headers, draftMode, params in layout.js, page.js, route.js, default.js, opengraph-image, twitter-image, icon and apple-icon, and searchParams in page.js.


            // Next.js 15 - synchronous access, deprecated
import { cookies } from 'next/headers'

export default function Page({ params, searchParams }) {
  const cookieStore = cookies()
  const token = cookieStore.get('token')
  const { slug } = params
  return <h1>{slug}</h1>
}
          

            // Next.js 16 - async only
import { cookies } from 'next/headers'

export default async function Page({ params, searchParams }) {
  const cookieStore = await cookies()
  const token = cookieStore.get('token')
  const { slug } = await params
  const query = await searchParams
  return <h1>{slug}</h1>
}
          

The typing story is better than most teams realise. Running npx next typegen generates globally available type helpers, PageProps, LayoutProps and RouteContext, which give type-safe access to the async props:


            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>
}
          

Two async changes are easy to miss because they live in metadata files rather than routes. In opengraph-image, twitter-image, icon and apple-icon, the image generating function now receives params and id as promises, while generateImageMetadata continues to receive synchronous params. The sitemap generating function likewise receives id as a promise, so generateSitemaps returning { id: 0 } now needs Number(await id) downstream rather than arithmetic on a raw number.

Turbopack by default, and the build that fails on purpose

Turbopack is stable and default for next dev and next build. You can delete the --turbopack flags from your package.json scripts.

The trap: if your project has a custom webpack configuration and you run next build, the build fails. That is deliberate, to prevent silent misconfiguration. Vercel's guide is direct about it, and adds a detail worth internalising: if you see a failing build complaining about a webpack configuration you did not write, a plugin is probably adding one.

Three ways out:


            {
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start"
  }
}
          

That keeps webpack for production while dev runs on Turbopack. The alternatives are running next build --turbopack to ignore the webpack config, or porting the config to Turbopack options properly. The honest recommendation is the middle path first and the migration second, because a bundler swap and a framework major in one pull request is how upgrades get reverted.

Turbopack configuration also moved out of experimental to the top level:


            import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  turbopack: {
    // options
  },
}

export default nextConfig
          

Two Turbopack behaviours bite existing codebases. Sass imports from node_modules no longer accept the legacy tilde prefix, so @import '~bootstrap/dist/css/bootstrap.min.css' becomes @import 'bootstrap/dist/css/bootstrap.min.css'. And client code importing Node.js native modules produces Module not found: Can't resolve 'fs' rather than being silenced by resolve.fallback. Turbopack offers turbopack.resolveAlias as an equivalent escape hatch, but Vercel's own guidance is to fix the import rather than alias it away. That is the right call. An alias hides a client bundle reaching for server code, and that is worth knowing about.

Cache Components, and why caching is now opt-in

This is the conceptual change, not a mechanical one. Cache Components centre on the "use cache" directive and replace the implicit caching of earlier App Router versions.

Tim Neutkens, co-author and tech lead for Next.js, and his colleagues on the Next.js team stated the reasoning in the Next.js 16 release post: "Unlike the implicit caching found in previous versions of the App Router, caching with Cache Components is entirely opt-in. All dynamic code in any page, layout, or API route is executed at request time by default, giving Next.js an out-of-the-box experience that's better aligned with what developers expect from a full-stack application framework."

Read that as a warning as much as a feature. If you were relying on implicit caching without knowing it, your pages now execute at request time by default, and the first thing you will notice after upgrading is a load pattern you did not have before.


            const nextConfig = {
  cacheComponents: true,
}

export default nextConfig
          

Cache Components absorb three previously separate flags: experimental.ppr, experimental.dynamicIO and experimental.useCache are all removed or deprecated in favour of cacheComponents. The route-level export const experimental_ppr is gone too.

One warning that deserves more attention than it gets. Vercel's guide says PPR in Next.js 16 works differently than in Next.js 15 canaries, and that if you are using PPR today you should stay on the Next.js 15 canary you are on. If you adopted PPR early, you are not on the standard upgrade path, and treating this as a routine bump will cost you.

The caching API changes that produce type errors

revalidateTag() now requires a cacheLife profile as its second argument. The single-argument form is deprecated and produces a TypeScript error.


            // Before
revalidateTag('posts')

// After - stale-while-revalidate
revalidateTag('posts', 'max')

// Or a built-in profile
revalidateTag('news-feed', 'hours')

// Or an inline expiry
revalidateTag('products', { expire: 3600 })
          

Two new Server Actions-only APIs sit alongside it, and the distinction between them matters more than the syntax:


            'use server'
import { updateTag, refresh } from 'next/cache'

// read-your-writes: user sees their own change immediately
export async function updateUserProfile(userId: string, profile: Profile) {
  await db.users.update(userId, profile)
  updateTag(`user-${userId}`)
}

// refresh uncached data only, cache untouched
export async function markNotificationAsRead(id: string) {
  await db.notifications.markAsRead(id)
  refresh()
}
          

Use revalidateTag with a profile where eventual consistency is acceptable, such as blog posts or product catalogues. Use updateTag in Server Actions where the user must see their own write immediately, such as forms and settings. Use refresh when the data was never cached. Mapping those three onto your existing revalidateTag calls is the one part of this migration that needs a human, because the old single-argument call site does not record which semantic you meant.

Also drop the unstable_ aliases, since cacheLife and cacheTag are now stable:


            // Before
import {
  unstable_cacheLife as cacheLife,
  unstable_cacheTag as cacheTag,
} from 'next/cache'

// After
import { cacheLife, cacheTag } from 'next/cache'
          

middleware.ts becomes proxy.ts, and loses the Edge runtime


            mv middleware.ts proxy.ts
          

Rename the exported function to proxy as well, even if you use a default export. Config flags carrying the old name change too: skipMiddlewareUrlNormalize becomes skipProxyUrlNormalize. The v16 codemod handles both.


            export function proxy(request: Request) {}
          

The constraint that decides whether this is a rename or a redesign: the Edge runtime is not supported in proxy. The proxy runtime is Node.js and cannot be configured. If you depend on Edge middleware, middleware.ts still exists but is deprecated and will be removed, and Vercel says it will follow up with Edge runtime instructions in a minor release. Teams running geo-routing or auth checks at the edge should treat this as an open item rather than a solved one.

The next/image defaults nobody reads until something looks wrong

Four defaults changed, and none of them fail a build. They change output quietly.

Setting Next.js 15 Next.js 16 Why it matters
images.minimumCacheTTL 60 seconds 4 hours (14400s) Cuts revalidation cost on images lacking cache-control
images.imageSizes Included 16 16 removed Smaller srcset; 16px was rare because DPR 2 fetches 32px
images.qualities All values 1 to 100 [75] only A quality={80} prop is coerced to 75
images.maximumRedirects Unlimited 3 Set 0 to disable
images.dangerouslyAllowLocalIP Allowed Blocked by default Set true only on private networks

The qualities change is the one that generates support tickets. If your team standardised on quality={90} for hero images, every one of them is now silently served at 75. Restore it explicitly if you need it:


            const nextConfig = {
  images: {
    qualities: [50, 75, 100],
    minimumCacheTTL: 60,
  },
}
          

Local image sources with query strings now require images.localPatterns.search configuration, which Vercel added to prevent enumeration attacks. images.domains is deprecated in favour of images.remotePatterns, and next/legacy/image is deprecated in favour of next/image.

Removals and quiet behaviour changes

next lint is removed, and next build no longer runs linting. If your CI relied on the build catching lint errors, it now does not. Migrate with npx @next/codemod@canary next-lint-to-eslint-cli . and add the ESLint step back to CI explicitly.

serverRuntimeConfig and publicRuntimeConfig are removed in favour of environment variables. For values that must be read at runtime rather than bundled at build time, call connection() first:


            import { connection } from 'next/server'

export default async function Page() {
  await connection()
  const config = process.env.RUNTIME_CONFIG
  return <p>{config}</p>
}
          

AMP is gone entirely, including useAmp and export const config = { amp: true }.

Three behaviour changes worth flagging to a platform team. next dev now outputs to .next/dev, separate from next build, so dev and build run concurrently, and a lockfile prevents two instances on one project. The size and First Load JS metrics are removed from next build output, because Vercel found them inaccurate in server-driven architectures using React Server Components. And during next dev, checking whether process.argv includes 'dev' inside your config file now returns false, because the config is no longer loaded twice. If a plugin triggers side effects on that check, it silently stops firing. Check process.env.NODE_ENV === 'development' instead.

That last one is the sort of thing that does not appear in any error message. It just stops working.

India-specific considerations

For teams in India shipping to a global audience, the images.minimumCacheTTL move from 60 seconds to 4 hours is the change with the clearest commercial upside. Image revalidation every 60 seconds on origin images lacking a cache-control header was a real cost line, and Vercel names increased CPU usage and cost as the reason for the change. Teams billing cloud spend in dollars while earning in rupees feel that arithmetic more sharply than most, and this is a default that now works in your favour without any code change. Our cloud FinOps guidance for Indian teams covers the wider pattern.

The Node.js 20.9 floor is the item to check first if you deploy to a managed Indian hosting platform or an older enterprise CI image, since Node.js 18 is no longer supported at all. Verify the runtime before you plan the sprint, not during it.

What we would sequence, and in what order

The order that survives contact with a real codebase:

Upgrade Node to 20.9 or later and TypeScript to 5.1 or later first, on its own branch, and merge it. Run the v16 codemod next and read every comment it leaves rather than trusting the diff. Add default.js to every parallel route slot before you attempt a build, because that failure produces a confusing message. Decide the bundler question deliberately: ship with next build --webpack if you have a custom config, and schedule the Turbopack migration separately. Rename middleware.ts to proxy.ts only after confirming you are not depending on the Edge runtime. Leave cacheComponents switched off for the initial upgrade. It is a programming model change, not a version bump, and mixing it into the same release makes any regression impossible to attribute.

The real cost here is not the codemod. It is the revalidateTag call sites where you have to remember what you meant.

FAQ

How eCorpIT can help

eCorpIT's senior engineering teams handle framework majors like this one for clients who cannot afford a stalled upgrade, and the work is mostly sequencing rather than heroics. We run the codemods, then audit what they left behind, which on Next.js 16 means the revalidateTag call sites and the Edge middleware assumptions that no tool can decide for you. We keep the bundler migration and the Cache Components adoption as separate releases from the version bump so regressions stay attributable. Talk to us if a Next.js upgrade has been sitting in your backlog because nobody wants to own the blast radius. Related work is covered in our custom web application development service and our QA and test automation service.

References

  1. Next.js 16 - Jimmy Lai, Josh Story, Sebastian Markbage and Tim Neutkens, Vercel, 21 October 2025
  1. How to upgrade to version 16 - Next.js documentation, version 16.2.10, last updated 13 May 2026
  1. Upgrading: Codemods - Next.js documentation
  1. Dynamic APIs are Asynchronous - Next.js documentation
  1. Getting Started: Upgrading - Next.js documentation
  1. Next.js 16 (beta) - Vercel
  1. Next.js 16.1 - Vercel
  1. Turbopack: What's New in Next.js 16.2 - Vercel
  1. Next.js 15 - Vercel
  1. Tim Neutkens, Co-Author of Next.js on the State of Next - This Dot Labs
  1. Releases: vercel/next.js - GitHub
  1. Next.js versions and support - endoflife.date

Last updated: 17 July 2026. Version numbers and configuration defaults verified against the official Next.js documentation on 17 July 2026.

Frequently asked

Quick answers.

01 When was Next.js 16 released and what version is current?
Next.js 16 was published on 21 October 2025. The official upgrade documentation tracks version 16.2.10 with a last-updated date of 13 May 2026. The release included Cache Components, stable Turbopack, React Compiler support, new caching APIs and React 19.2 features.
02 What are the minimum requirements for Next.js 16?
Node.js 20.9.0 or later, since Node.js 18 is no longer supported, and TypeScript 5.1.0 or later. Browser support requires Chrome 111 or later, Edge 111 or later, Firefox 111 or later, and Safari 16.4 or later. Check the Node version before planning the upgrade work.
03 Why does my Next.js 16 build fail with a webpack configuration?
Turbopack is the default bundler for next build in Next.js 16, and a project with a custom webpack configuration fails the build deliberately to prevent misconfiguration. Run next build --webpack to opt out, next build --turbopack to ignore the config, or migrate the config to Turbopack options.
04 Do I have to rewrite every page for async request APIs?
Mostly no. The next-async-request-api codemod handles the bulk of the transformation, awaiting or wrapping the calls. Where automatic migration is impossible it inserts a typecast or comment for manual review. Run npx next typegen to generate the PageProps, LayoutProps and RouteContext type helpers.
05 What replaces middleware.ts in Next.js 16?
proxy.ts replaces middleware.ts, and the exported function should be renamed to proxy. The Edge runtime is not supported in proxy, whose runtime is Node.js and cannot be configured. Teams needing the Edge runtime can keep using the deprecated middleware.ts for now.
06 Should I enable Cache Components during the upgrade?
We would not. Cache Components make caching entirely opt-in through the "use cache" directive, meaning all dynamic code executes at request time by default. That is a programming model change rather than a version bump, so enabling it in the same release makes regressions hard to attribute to a cause.
07 Why did my image quality drop after upgrading?
The images.qualities default changed from allowing every value between 1 and 100 to only [75]. A quality prop outside that array is coerced to the closest permitted value, so quality={80} now renders at 75. Set images.qualities: [50, 75, 100] in your config to restore other levels.
08 Is it safe to upgrade if I already use Partial Prerendering?
No, not as a routine bump. Vercel's guide states that PPR in Next.js 16 works differently than in Next.js 15 canaries, and advises teams currently using PPR to stay on their existing Next.js 15 canary. The experimental.ppr flag and route-level experimental_ppr export are both removed.

About the author

Manu Shukla

Founder & Director

Founder of eCorpIT. Hands-on engineer leading senior-only delivery for AI apps, custom software, and cloud systems for global clients.

Subscribe

One engineering note a week. No fluff, no spam.

Senior-architect playbooks on AI agents, mobile apps, cloud, security, data, and marketing — delivered every Wednesday.

Past the reading

Read enough. Let's build something.

A senior architect responds in 24 working hours with scope, indicative cost, and a timeline. NDA before any technical conversation.