Next.js 15: What Changed and Should You Upgrade?

I walk through the two breaking changes in Next.js 15 — async request APIs and the new caching defaults — explain the React 19 requirement, and help you decide if it's time to upgrade.

Next.js 15: What Changed and Should You Upgrade?

If you learned the App Router from tutorials written during version 13 or 14, two of the things you absorbed are now wrong. Not edge-case wrong — they’re wrong in the patterns you use every day. The way cookies() and headers() work in Server Components changed. The caching behaviour you relied on for data fetching reversed. And params — the prop you’ve been destructuring in every dynamic route — is now a Promise.

None of this is buried in the changelog. The two breaking changes in Next.js 15 land directly in the patterns the App Router introduced as defaults. If your project uses the App Router and you haven’t upgraded yet, understanding what changed will save you a confused afternoon when you finally do.

I’ll walk through exactly what changed, why the team made these decisions, what the migration looks like in code, and give you a clear framework for deciding whether to upgrade now or wait.

On this page

Two things break when you upgrade — the rest is new features

Next.js 15 landed as stable in October 2024, building on two release candidates that shipped earlier that year. The official announcement covers everything, but the change list is long and not everything matters equally.

Two changes affect the code you’re already running:

  1. Async Request APIs — cookies(), headers(), draftMode(), params, and searchParams are now asynchronous. If you call them without await, your app throws at runtime.
  2. Caching defaults reversed — fetch() calls, GET Route Handlers, and the Client Router Cache are no longer cached by default. In version 14, everything was cached and you opted out. In version 15, nothing is cached and you opt in.

Everything else — Turbopack going stable, React 19 support, next.config.ts, the new <Form> component, Server Action security improvements — ships as an addition, not a break. Worth knowing, but not something that breaks existing code.

Async request APIs: cookies, headers, and params now return Promises

This is the change that produces the most type errors on a fresh upgrade.

In Next.js 14, you called cookies() or headers() synchronously at the top level of a Server Component:

import { cookies } from 'next/headers'

export default function AdminPage() {
  const cookieStore = cookies()
  const token = cookieStore.get('token')
  // ...
}

In Next.js 15, these functions return a Promise, so you need to await them. The component also becomes async:

import { cookies } from 'next/headers'

export default async function AdminPage() {
  const cookieStore = await cookies()
  const token = cookieStore.get('token')
  // ...
}

The same applies to headers(), draftMode(), and — the one that surprises people most — route params. What used to be typed as { slug: string } is now Promise<{ slug: string }>:

// Next.js 15 — params is a Promise
type Params = Promise<{ slug: string }>

export default async function BlogPost({ params }: { params: Params }) {
  const { slug } = await params
  return <article>{slug}</article>
}

For synchronous components, React’s use() hook handles unwrapping the Promise without needing async:

import { use } from 'react'

type Params = Promise<{ slug: string }>

export default function BlogPost(props: { params: Params }) {
  const params = use(props.params)
  return <article>{params.slug}</article>
}

The reason for this change is about enabling better streaming. When cookies() is synchronous, Next.js has to wait for a request before rendering anything — the entire component tree blocks. Making it async lets the framework start rendering components that don’t need request data, then pause only at the actual await. The goal is to lay the groundwork for Partial Prerendering.

The automated codemod handles most of this migration: npx @next/codemod@canary next-async-request-api .. It won’t catch every case — anything where params flows through multiple layers of components will need manual review — but it eliminates the straightforward transformations.

If you’re not yet familiar with how Server Components handle async data generally, the Server vs Client Components guide covers the underlying mental model. The App Router getting-started post covers the file conventions these components live in.

Fetch and the Router Cache are no longer cached by default

This is the change that catches people after the type errors are fixed.

In Next.js 14, a plain fetch() inside a Server Component was cached indefinitely unless you explicitly opted out:

// Next.js 14 — cached by default, no request made on subsequent renders
const data = await fetch('https://api.example.com/posts')

// To disable caching in v14
const data = await fetch('https://api.example.com/posts', { cache: 'no-store' })

In Next.js 15, the default flipped. A plain fetch() behaves like a normal network request — uncached, called on every render:

// Next.js 15 — NOT cached, request fires every time
const data = await fetch('https://api.example.com/posts')

// To opt into caching in v15
const data = await fetch('https://api.example.com/posts', { cache: 'force-cache' })

The Client Router Cache changed too. In version 14, page segments were cached after the first navigation, so clicking around the app showed stale data until you revalidated. In version 15, the default staleTime for page segments dropped to 0 — the client always requests fresh data when navigating. Shared layout data is still cached (to support partial rendering), and back/forward navigation still restores from cache for scroll position.

If you want to restore the version 14 Router Cache behaviour, you can opt back in via config:

// next.config.ts
const nextConfig: NextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30, // seconds
    },
  },
}

The team’s reasoning is honest: the v14 caching defaults confused more people than they helped. Developers accidentally served stale data, or had to add cache: 'no-store' everywhere to get expected behaviour. The new defaults match how you’d expect a server request to behave unless you deliberately add caching.

If you have existing ISR-style patterns or heavy data caching, these will need to be made explicit. I covered the three caching layers — the Request Memoisation cache, the Data Cache, and the Router Cache — and when each applies in the data fetching and caching guide.

React 19 ships with the App Router; Pages Router keeps React 18

The App Router in Next.js 15 requires React 19. This is the stable release from December 2024 — not RC, not experimental.

That means useFormState (deprecated in React 19 in favour of useActionState), ref as a regular prop, improved hydration error messages, and the use() hook for reading Promises are all available when you upgrade. I covered what each of these means for day-to-day development in the React 19 guide.

One nuance that matters if you have a mixed codebase: Next.js 15 is backward-compatible with React 18 specifically for the Pages Router. You can run the Pages Router on React 18 and the App Router on React 19 in the same app. The official docs say this setup is possible but not recommended — the two React versions can produce type inconsistencies and unpredictable behaviour in edge cases. Worth knowing as a temporary migration path, not as a long-term architecture.

The React Compiler also arrives as an experimental option in this release. It’s a Babel plugin at this stage, which means slower builds. The documentation recommends against adopting it unless you have a concrete reason to try it, and the stability of its output is still being validated across real projects.

Turbopack graduates and the config file gets TypeScript support

Two changes that improve the developer experience without touching application code.

Turbopack dev is stable. You can now run next dev --turbo without it being experimental. The improvements are meaningful on large apps: the Next.js team reported up to 76.7% faster local server startup and up to 96.3% faster Fast Refresh on their own production codebase. If you’ve been avoiding the flag because it felt risky, it’s safe to switch on.

next.config.ts is now supported. Renaming next.config.js to next.config.ts gives you TypeScript autocomplete and type checking on config options:

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // TypeScript will error if you mistype an option name
}

export default nextConfig

Two stable config renames also ship with this version:

  • experimental.serverComponentsExternalPackages → serverExternalPackages
  • experimental.bundlePagesExternals → bundlePagesRouterDependencies

These move out of experimental and get cleaner names. If your config file references the old experimental paths, you’ll see a warning or error at build time. The upgrade codemod handles both renames automatically.

Three mistakes that look like bugs mid-upgrade

1. Missing await in generateMetadata. The async change applies to every function that receives params — including generateMetadata. This one gets missed because it lives in the same file as the page component but looks structurally different:

type Params = Promise<{ slug: string }>

// This also needs await — it's not just the page component
export async function generateMetadata({ params }: { params: Params }) {
  const { slug } = await params
  return { title: slug }
}

2. Silent fetch cache regression in production. A component that worked correctly in version 14 — because the fetch response was cached — may appear to work in development under version 15 (development mode skips some caching anyway). The problem surfaces in production when uncached requests fire on every render and start hitting rate limits or causing noticeable latency. Audit fetch calls that relied on caching and add { cache: 'force-cache' } explicitly.

3. The experimental-edge runtime string. If any route file exports export const runtime = "experimental-edge", that string is now an error in Next.js 15. Change it to "edge". The upgrade codemod handles this, but if you applied the codemod before updating all your files, stragglers can show up as unexpected build failures.

Check this before running the upgrade

  • Run npx @next/codemod@canary upgrade latest to automate the mechanical changes
  • Grep for cookies(), headers(), draftMode(), and params — confirm each is awaited
  • Check fetch() calls that previously relied on default caching — add { cache: 'force-cache' } where needed
  • Check generateMetadata functions that destructure params — add await
  • Update renamed config options: serverExternalPackages, bundlePagesRouterDependencies
  • Replace any @next/font imports with next/font

Upgrade when the async change is the hardest work ahead of you

The decision depends mostly on how spread out your request-specific APIs are.

Your situation Recommended approach Why
New project Start on Next.js 15 directly No migration — you get the current defaults from day one
Existing App Router project, few dynamic routes Upgrade with a half-day of focused work The codemod handles most of it; review the remaining hits manually
Existing App Router project, many dynamic routes Plan a proper migration sprint Every params, cookies(), and headers() call needs review
Pages Router only Upgrade at your own pace React 18 compatibility is preserved; no breaking changes for Pages Router
Active production app mid-feature rollout Finish the current feature first The caching reversal can quietly change performance characteristics

The async API change is real work, but it’s bounded work — you can search for every call site and fix them one by one. What the codemod can’t do is decide which fetch() calls should be cached and which should always be fresh. That judgment call is the part worth spending time on before you upgrade a production app.

The caching reversal is, in my view, the more meaningful improvement. Opt-in caching means you can read a component and understand its data freshness behaviour without also knowing the global defaults. The code says what it does. That’s worth the migration even if it takes a sprint.

Abstract geometric composition with concentric arcs and arrows connecting to stacked version layers, with the top layer highlighted in accent colour — representing an async upgrade path with explicit direction.

The async API change and the caching reversal are the two things that need active migration. Everything else — Turbopack, React 19, TypeScript config — adds capability without breaking what exists.

Sources