Next.js 16 Migration Guide: Fixing the Silent Breakages
In short
Four changes in Next.js 16 can break a production app without throwing a single error: the middleware.ts to proxy.ts rename, the deprecated single-argument revalidateTag, caching flipping to opt-in, and async params. I migrated my production reputation SaaS to 16.2 in about 14.5 hours spread over three days, and every problem I hit was a correctness bug, not a crash. This post is the checklist the official upgrade guide (last updated May 13, 2026) does not give you: the symptom, the root cause, the exact fix, and how you would actually notice each one in production.

On this page
- What breaks silently when you upgrade to Next.js 16?
- Why is middleware.ts silently ignored?
- Why does revalidateTag serve stale data now?
- Why did my static pages suddenly go dynamic?
- What changed about params and searchParams?
- 'use cache' vs ISR vs fetch caching: which one should you use?
- What breaks when Turbopack becomes the default build?
- Which changes does the codemod handle, and which are manual?
- How long does a Next.js 16 migration take on a production app?
- Should you wait for Next.js 16.3?
- Key takeaways
What breaks silently when you upgrade to Next.js 16?
Four changes ship with no build error and no runtime exception: middleware.ts is ignored until you rename it to proxy.ts, single-argument revalidateTag serves stale data, caching is now opt-in so formerly static routes go dynamic, and params became a Promise so property access returns undefined in plain JavaScript. Everything compiles. Everything deploys. The bugs show up as wrong behavior, not red text.
Version context first, because it matters for what you are upgrading into: Next.js 16 went GA on October 21, 2025, 16.2 shipped on March 18, 2026 with roughly 400% faster dev startup, and 16.3 has been in canary since June 4, 2026.
Here is the table I wish I had before I started:
| Change | Symptom | Fix |
|---|---|---|
middleware.ts renamed to proxy.ts | Auth redirects, rewrites, and header rules silently stop running | Rename the file to proxy.ts, export proxy, delete runtime: 'edge' |
Single-arg revalidateTag(tag) deprecated | Mutations save, but the next request still shows stale data | updateTag(tag) in Server Actions, or revalidateTag(tag, 'max') |
| Caching flipped to opt-in | Formerly static/ISR routes render on every request; TTFB and DB load climb | Add 'use cache' with cacheLife/cacheTag to routes that should cache |
params/searchParams are async | params.id is undefined; queries return empty results or 404s | await params (the codemod rewrites most call sites) |
Why is middleware.ts silently ignored?
middleware.ts is silently ignored in Next.js 16; it must be renamed proxy.ts and runs on the Node.js runtime only, because the edge runtime for it was removed. There is no build warning loud enough to stop you. On my staging deploy, the symptom was a smoke test hitting /dashboard logged out and getting a 200 instead of a 307 redirect to /login. If I had shipped that to production, every protected route in my reputation SaaS would have been publicly reachable.
The fix is mechanical:
// proxy.ts (was middleware.ts)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export default function proxy(request: NextRequest) {
const session = request.cookies.get('session')
if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = { matcher: '/dashboard/:path*' }Also delete any export const runtime = 'edge' and replace edge-only APIs. The upside: you now have the full Node.js API surface in your proxy, so things like crypto and proper JWT libraries just work.
Why does revalidateTag serve stale data now?
The single-argument revalidateTag('reviews') form is deprecated in Next.js 16 and behaves as stale-while-revalidate: the next request after the mutation still gets the old cached entry, and only the request after that sees fresh data. There is no build error and no console warning at the call site, which is why people end up googling "revalidateTag not working Next.js 16" instead of reading a stack trace.
In my SaaS the symptom was specific: a business owner approves an AI-drafted reply, the Server Action succeeds, and the review list still shows the reply as pending until a second refresh. The fix is updateTag('reviews') inside Server Actions when you need read-your-own-writes, or revalidateTag('reviews', 'max') elsewhere for an immediate expire. This matters more than it used to, because pages are no longer only read by humans. I covered in what WebMCP means for web apps and AI agents ↗ how agents increasingly consume your pages directly, and an agent will happily act on stale data you forgot to expire.
Why did my static pages suddenly go dynamic?
Next.js 16 makes caching opt-in. Nothing is cached unless you say so, which means routes that were static or ISR in 15 quietly become fully dynamic and hit your data layer on every request. No error, just worse numbers. On my public review pages, p95 TTFB went from about 120ms to about 460ms and Postgres CPU roughly doubled until I added explicit caching back.
You notice this one in dashboards, not logs: rising TTFB, rising database connections, rising serverless invocation costs. If you do not have those graphs, set them up before you migrate.
What changed about params and searchParams?
params and searchParams are now Promises with no synchronous fallback. TypeScript catches this at compile time, but in plain JavaScript params.id on a Promise is simply undefined, so your query runs with an undefined ID and you get empty pages or 404s. The Next.js 15 warning string, params should be awaited before using its properties, is gone; the grace period ended. The fix is const { id } = await params, and the official codemod handles the overwhelming majority of these.
'use cache' vs ISR vs fetch caching: which one should you use?
Use 'use cache' for anything you control end to end, keep fetch-level caching for third-party APIs, and treat classic ISR exports as legacy that still works but is no longer the forward path. 'use cache' is a directive you place at the top of a file, component, or async function to tell Next.js to cache its output until one of its tags is revalidated or its cacheLife expires. This is the single most confused concept of the release, so here is the decision table I now use:
| Data freshness need | Typical route | Use |
|---|---|---|
| Changes only on deploy | Marketing pages, docs | 'use cache' with default cacheLife |
| Refresh on a schedule (minutes to hours) | Public listings, blog, review widgets | 'use cache' + cacheLife('hours') (the ISR replacement) |
| Refresh after user mutations | Authed pages mutated by Server Actions | 'use cache' + cacheTag + updateTag |
| Caching someone else's API response | Any route calling third-party APIs | fetch(url, { next: { revalidate: 300 } }) |
| Always fresh, per-user | Admin panels, billing, anything with PII | No cache, stay dynamic |
The mental shift: in 15 you fought the framework to make things dynamic; in 16 you declare what is cacheable and everything else is dynamic by default. My rule on the SaaS was simple. Public, shared, tolerant of minutes of staleness: cache it. Authenticated or money-related: do not.

What breaks when Turbopack becomes the default build?
Turbopack is the default for both dev and production builds in Next.js 16, and your webpack() config in next.config is ignored. The tell is this exact line in your build output: Webpack is configured while Turbopack is not, which may cause problems. Anything that depended on a custom loader breaks next, usually SVGR, with Module not found: Can't resolve errors or SVGs importing as URLs instead of components.
The fix is to port loaders to turbopack.rules:
// next.config.ts
const nextConfig = {
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
}
export default nextConfigIf a plugin has no Turbopack equivalent yet, next build --webpack is the escape hatch, but treat it as temporary. The payoff is real: on my machine, dev cold start dropped from about 7.5s to under 2s after 16.2, and the production build went from 2m40s to a bit over a minute. That alone justified the migration for me.
Which changes does the codemod handle, and which are manual?
Run npx @next/codemod@canary upgrade latest first; it reliably handles the syntactic changes and none of the semantic ones. On my codebase it caught every async params/searchParams rewrite in TypeScript files, the import renames, and the config key moves. That is the easy half.
What it cannot do for you:
- Decide what should be cached. The opt-in caching audit is judgment work, route by route.
- Fix revalidateTag semantics. It will not choose between
updateTagandrevalidateTag(tag, 'max')because that depends on your UX. - Validate your proxy.ts logic. The rename is trivial; confirming your matchers and auth still behave is not.
- Port webpack loaders.
turbopack.rulesneeds a human who knows why each loader existed.
A loud failure costs you an afternoon. A silent failure costs you a week, because you spend the first six days not knowing it exists.
How long does a Next.js 16 migration take on a production app?
About 14.5 hours for my reputation SaaS: roughly 38 routes, 11 Server Actions, two cron jobs, one developer. Here is the actual breakdown, so you can budget against your own route count:
- Read the upgrade guide, audit dependencies for Turbopack support: 1.5 h
- Run the codemod, review the diff: 1 h
- Fix async params leftovers in plain JS utility files the codemod missed: 2 h
- Rename middleware.ts to proxy.ts, remove edge runtime assumptions: 1 h
- Sweep every revalidateTag call, convert to updateTag where actions need fresh reads: 1.5 h
- Route-by-route cache audit, adding 'use cache', cacheLife, and cacheTag: 4 h
- Turbopack build fixes (SVGR, one Sass quirk): 1.5 h
- Staging deploy with route-by-route verification of cache behavior and auth: 2 h
Step 6 dominates, and it scales with route count, not codebase size. The verification pass in step 8 reuses the same discipline I described in my audit checklist for fixing vibe-coded apps ↗: open every route, check headers, confirm the cache state you expect. A good chunk of my app rescue and optimization ↗ work this spring has been exactly this, untangling Next.js 16 upgrades where someone shipped the codemod output straight to production and only noticed when customers reported stale dashboards.
Should you wait for Next.js 16.3?
No. Migrate to 16.2 now; it shipped on March 18, 2026, has had almost three months of patch releases, and the dev startup improvement is worth the effort by itself. 16.3 has been in canary since June 4, 2026 and is incremental, not corrective. There is no breaking change on the horizon that would invalidate work you do today.
My verdict after doing it on a revenue-generating app: Next.js 16 is worth migrating to for the dev-server speed alone, but do it behind a staging deploy with route-by-route cache verification, because the failures are correctness bugs, not crashes. Nothing will page you. Your error tracker stays green while users see stale data and your middleware does nothing. Every greenfield build in my MVP development ↗ work starts on 16.2 now, and it is the default across my web development ↗ client projects, but I will not upgrade an existing production app without that verification pass.
Key takeaways
- Four Next.js 16 changes break apps with zero errors: the proxy.ts rename, deprecated single-arg revalidateTag, opt-in caching, and async params.
- middleware.ts is silently ignored: rename it to proxy.ts, and note it runs on the Node.js runtime only since edge support was removed.
- Caching is now opt-in: routes that were static in 15 go dynamic in 16, so watch TTFB and database load, not error logs.
- The codemod handles syntax, not semantics: async params get rewritten for you; cache decisions and revalidation behavior do not.
- Budget by route count: my 38-route production SaaS took about 14.5 hours, and the route-by-route cache audit was the largest single step at 4 hours.
FAQ
Why is my middleware.ts not running in Next.js 16?
Because Next.js 16 renamed it. middleware.ts is silently ignored; the file must be called proxy.ts and export a `proxy` function, and it runs on the Node.js runtime only since edge support was removed. There is no error, so auth redirects and rewrites just stop. Rename the file and remove any `runtime: 'edge'` config.
Is revalidateTag deprecated?
The single-argument form is. In Next.js 16, `revalidateTag('tag')` behaves as stale-while-revalidate, so the next request still serves old data. Use `updateTag('tag')` inside Server Actions when the user must immediately see their own change, or `revalidateTag('tag', 'max')` for an immediate expire from webhooks and route handlers.
Can I still use webpack with Next.js 16?
Yes, as an escape hatch. Turbopack is the default for dev and production builds, and your `webpack()` config is ignored with a warning. Run `next build --webpack` to opt back in temporarily, but plan to port custom loaders to `turbopack.rules`, since the webpack path is clearly in maintenance mode.
Do I have to rewrite ISR routes to 'use cache'?
Not immediately. The classic `revalidate` export still works in Next.js 16, so existing ISR routes keep functioning. The forward path is `'use cache'` with `cacheLife`, which gives the same scheduled-refresh behavior with finer control. I migrated mine during the cache audit, but it is the one change you can safely defer.
Working on something like this?
I build web apps, AI features, and mobile products for clients. If this article matches a problem you have, tell me about it.
Start a conversationMalik Hamza Shabbir · Full-Stack & AI Engineer
I build full-stack and AI products solo: a reputation SaaS in production, RAG pipelines, and React Native apps. I write from what I ship, not from documentation summaries.
Run it on yours
Every number above came from code you can read.
The harness is open and the per-probe log ships with it. If you build one of these systems, or you are choosing between them with real money, the same method is available to you.
Send me an endpoint and I will write an adapter, run it on the same workload under the same rules, and publish the result. It goes in the table whether it wins or not.
Sponsored inclusion is disclosed in the article and the result publishes either way. Paying moves you up the queue, never up the table.
Related articles
How to Fix a Vibe-Coded App: My Rescue Audit Checklist
Roughly 8,000 of 10,000 AI-built startup apps need rescue work in 2026. Here is the 60-minute triage and fix order I use to save them without a rewrite.
GEO in 2026: Getting Cited by ChatGPT and Perplexity
GEO means writing answer-first chunks AI engines can lift: 69% of Google searches are zero-click in 2026, but ChatGPT referrals convert at 15.9%.
Leaving Vercel in 2026: The Real Self-Hosting Cost Math
Self-hosting Next.js on Hetzner with Coolify costs me $6-17/mo vs $40-150 on Vercel Pro. The real math, ops hours included, after the April 2026 breach.