Surviving the Next.js 15 Upgrade: A Small Team's Battle-Tested Migration Strategy

By amillionmonkeys
#Next.js#React#Web Development#Migration

Real migration issues we hit upgrading to Next.js 15 and React 19, including async request APIs, caching bugs, and third-party library compatibility problems.

We upgraded our first production app to Next.js 15 three weeks after the stable release dropped. On paper, it looked straightforward—mostly opt-in features, a few async API changes, and the promise of better performance. In reality, we hit five production-breaking issues in the first hour of testing.

If you're planning a Next.js 15 migration, here's what actually broke, how we fixed it, and the migration strategy we're now using for every client app we upgrade.

What Actually Changed in Next.js 15

The Next.js 15 release brought React 19 support, async request APIs, updated caching defaults, and Turbopack improvements. Most guides focus on the shiny new features. We're focusing on the stuff that breaks.

The three biggest breaking changes we encountered:

  • Async Request APIs: cookies(), headers(), params, and searchParams are now async
  • Caching behaviour changes: GET route handlers and client-side Router Cache now opt-out by default
  • React 19 compatibility: Third-party libraries using deprecated React APIs throw errors

None of these are showstoppers, but each required careful testing across our codebase.

Our Migration Approach: Test First, Deploy Never (Until It Works)

Here's the strategy that saved us from a 2am rollback:

  1. Clone production environment locally with real data
  2. Upgrade in isolated branch with comprehensive testing
  3. Fix breaking changes one by one (we'll detail these below)
  4. Test third-party integrations separately (analytics, auth, payment processors)
  5. Deploy to staging and run automated test suite
  6. Monitor for 48 hours before touching production

This sounds obvious, but we've seen teams upgrade dependencies on a Friday afternoon and hope for the best. Don't be those teams.

Breaking Change #1: Async Request APIs

The biggest code change was converting synchronous request APIs to async. In Next.js 14, you could do this:

// Next.js 14 - synchronous
export default function ProductPage({ params }) {
  const { id } = params;
  const cookieStore = cookies();
  const theme = cookieStore.get('theme');
 
  return <Product id={id} theme={theme} />;
}

In Next.js 15, those APIs are async:

// Next.js 15 - async
export default async function ProductPage({ params }) {
  const { id } = await params;
  const cookieStore = await cookies();
  const theme = cookieStore.get('theme');
 
  return <Product id={id} theme={theme} />;
}

What Broke

We had 47 server components using params or searchParams. Every single one needed the async keyword and await operators added. Our linter didn't catch these—TypeScript did, but only after we ran type checking.

How We Fixed It

We used a combination of find-and-replace and manual review:

  1. Searched for function.*\{.*params to find all usages
  2. Added async to function signatures
  3. Added await to params, searchParams, cookies(), and headers()
  4. Re-ran TypeScript (npm run type-check) until errors cleared

Time cost: About 3 hours across a mid-sized app with 50+ server components.

Breaking Change #2: Caching Defaults

Next.js 15 changed default caching behaviour for GET route handlers and the client-side Router Cache. Previously, both cached by default. Now, they opt-out by default.

This broke our /api/products endpoint, which relied on automatic caching to handle high traffic. After upgrading, response times went from 50ms to 800ms under load because every request hit our database.

How We Fixed It

We explicitly opted back into caching where it made sense:

// Next.js 15 - explicit caching
export async function GET() {
  const products = await db.products.findMany();
 
  return Response.json(products, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'
    }
  });
}

For data fetching, we added cache configuration to fetch() calls:

// Explicitly cache fetch requests
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache', // or 'no-store' to opt-out
  next: { revalidate: 3600 } // revalidate every hour
});

Lesson learned: Don't assume default behaviour stays the same across major versions. Test performance under realistic load before deploying.

Breaking Change #3: React 19 and Third-Party Libraries

Next.js 15's React 19 support is great—until you discover your UI library still uses deprecated React APIs.

We hit compatibility issues with:

  • Older UI component libraries using legacy context API
  • Analytics packages calling removed React methods
  • Form libraries expecting synchronous render behaviour

How We Fixed It

We created a compatibility testing checklist:

  1. Checked each dependency's GitHub issues for "React 19" mentions
  2. Updated libraries with React 19 support available
  3. Found alternatives for libraries without support
  4. Used Next.js's package.json overrides to force compatible peer dependencies (temporary fix only)

Example override (use cautiously):

{
  "overrides": {
    "some-old-library": {
      "react": "^19.0.0"
    }
  }
}

We also discovered that some analytics scripts needed to be loaded differently. If you're using tools like Google Analytics or Hotjar, test their tracking after upgrading—we found event tracking broke silently.

What Didn't Break (But We Expected It To)

Surprisingly, a few things worked perfectly:

  • TypeScript integration: No changes needed to our tsconfig.json
  • Existing Server Actions: Worked without modification
  • Image optimization: next/image continued working as expected
  • Middleware: Our auth middleware required zero changes

Lessons From Four Production Migrations

We've now completed Next.js 15 migrations for four client apps. Here's what we learned:

Start with your smallest app. We migrated our internal dashboard first, which gave us a safe environment to discover issues without client impact.

Budget 2-3x your estimate. Our first migration took 8 hours instead of the planned 3 hours. Third-party compatibility testing was the time sink.

Test payment flows manually. Automated tests missed a Stripe integration issue that only appeared during actual checkout. Always test critical user flows by hand.

Monitor error tracking religiously. We use Sentry, and it caught three issues in production that slipped through staging. Set up alerts before deploying.

Have a rollback plan. Keep the previous Next.js 14 deployment ready to restore. We've never needed it, but knowing it's there reduces deployment stress.

Should You Upgrade Right Now?

It depends on what you're building.

Upgrade now if:

  • You need React 19 features (like improved Server Components)
  • You want better Turbopack performance in development
  • You're starting a new project

Wait if:

  • You rely heavily on third-party libraries without React 19 support
  • You have complex caching strategies that need revalidation testing
  • Your team doesn't have bandwidth to troubleshoot edge cases

We're upgrading all new client projects to Next.js 15 by default, but we're taking a measured approach with existing production apps—upgrading when there's a clear performance or feature benefit, not just for the sake of being on the latest version.

Key Takeaways

If you're planning a Next.js 15 migration:

  • Async request APIs: Add async/await to any code using params, searchParams, cookies(), or headers()
  • Caching changes: Explicitly configure cache behaviour; don't rely on defaults
  • Third-party compatibility: Test every dependency, especially UI libraries and analytics
  • Plan for surprises: Budget extra time for unexpected edge cases
  • Test payment flows manually: Critical user journeys need hands-on testing
  • Keep monitoring: Use error tracking to catch issues that slip through testing

The upgrade itself isn't painful if you're methodical. The pain comes from rushing it or assuming backwards compatibility.

Need help with a Next.js 15 migration? We've developed a repeatable migration process that minimizes downtime and catches breaking changes before they hit production. Get in touch to discuss your upgrade timeline.

T: 07512 944360 | E: [email protected]

© 2025 amillionmonkeys ltd. All rights reserved.