Surviving the Next.js 15 Upgrade: A Small Team's Battle-Tested Migration Strategy
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, andsearchParamsare 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:
- Clone production environment locally with real data
- Upgrade in isolated branch with comprehensive testing
- Fix breaking changes one by one (we'll detail these below)
- Test third-party integrations separately (analytics, auth, payment processors)
- Deploy to staging and run automated test suite
- 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:
- Searched for
function.*\{.*paramsto find all usages - Added
asyncto function signatures - Added
awaittoparams,searchParams,cookies(), andheaders() - 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:
- Checked each dependency's GitHub issues for "React 19" mentions
- Updated libraries with React 19 support available
- Found alternatives for libraries without support
- Used Next.js's
package.jsonoverrides 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/imagecontinued 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/awaitto any code usingparams,searchParams,cookies(), orheaders() - 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.