Migrating Pages Router to App Router in Next.js 16 Without Downtime
The dangerous part of this migration was never the file renaming. pages/about.tsx becoming app/about/page.tsx is mechanical. What actually takes down production is treating "move to the App Router" as a single migration when it is, structurally, three unrelated migrations wearing one trench coat: a runtime change in how requests are intercepted before they hit either router, a rendering-model change in what gets cached by default, and a bundler change in how the whole thing gets compiled. Teams that flip all three at once lose the ability to tell which one broke when something breaks. Teams that separate them can roll back one layer without touching the other two.
Next.js 16 does make the coexistence itself safe. The app/ and pages/ directories can run in the same deployment indefinitely, and where a route exists in both, app/ takes precedence — that precedence rule is what makes route-by-route migration possible instead of a big-bang rewrite. The rest of this piece is about the parts that precedence rule doesn't cover.
The Infrastructure Prerequisite That Breaks Builds Before Any Routing Code Changes
Next.js 16 requires Node.js 20.9 or later; Node 18 support was removed outright. This sounds like a footnote until a team upgrades next in package.json, pushes to a CI runner still pinned to a Node 18 base image, and watches the build fail with errors that look like migration bugs but are actually toolchain mismatches. The fix has nothing to do with routers — it's updating Docker base images, CI runner configuration, and any .nvmrc files — but it has to happen before a single file moves from pages/ to app/, or every subsequent failure gets misdiagnosed as a routing problem.
The same version bump pulls in a React 19.2 requirement, since Server Components and the other App Router primitives don't exist on React 18. If the codebase has any dependency pinned against React 18 peer ranges, that surfaces at the same moment — another reason to treat the environment upgrade as its own completed step with its own green build, not something to discover mid-router-migration.
What the Official Codemod Fixes, and Where It Stops
Vercel ships an automated codemod — npx @next/codemod@canary upgrade latest — that handles the bulk of the mechanical conversion: moving Turbopack configuration, renaming middleware.ts references, stripping the unstable_ prefix from cacheTag and cacheLife, removing the experimental_ppr segment config, and converting the request APIs (params, searchParams, cookies(), headers(), draftMode()) to their now-required async form. Running it first and committing the result before any manual changes gives a clean diff to work from.
What it doesn't fix is anything involving next/router. The App Router replaces it with three separate hooks from next/navigation — useRouter, usePathname, useSearchParams — and the two are not interchangeable. A shared component imported by both an unmigrated pages/ route and a migrated app/ route can't call useRouter() from next/router in one context and next/navigation in the other without either forking the component or feature-detecting which tree it's rendering in. Teams with internationalization libraries built against the pages directory hit a related wall: if the i18n provider is wired through _app.tsx, every component importing it stays locked to the Pages Router until that provider is rebuilt for app/layout.tsx, which in practice forces certain shared components to migrate as a block rather than individually — this is where "route by route" migration plans quietly turn into "route cluster by route cluster."
proxy.ts vs. middleware.ts: A Runtime Boundary, Not a Rename
Next.js 16 replaces middleware.ts with proxy.ts, and the change is not cosmetic. Middleware ran on the Edge Runtime, meaning no filesystem access, no arbitrary npm packages, no Node-only APIs. proxy.ts runs on the Node.js runtime by default.
middleware.ts vs. proxy.ts — same job (intercepting requests before they reach a route), different execution environment: Edge Runtime versus Node.js runtime, with proxy.ts gaining access to fs, crypto, and native modules that were previously unreachable at that layer.
For a zero-downtime migration, that runtime swap matters more than the rename does. Any authentication or session-validation logic sitting in middleware — reading a cookie, verifying a signature, redirecting unauthenticated requests — is now executing in a different runtime with different available APIs and different cold-start characteristics. That logic deserves its own test pass, decoupled from the page migration entirely, before it's trusted with production auth traffic. The good news is that sequencing is optional: middleware.ts is deprecated but still functional in Next.js 16, so a team can leave it in place, finish the route-by-route App Router migration first, and move the proxy layer last, once it's the only remaining unknown.
Turbopack as the Default Introduces a Second Variable Into Every Failed Build
Turbopack is now the default bundler for both next dev and next build; webpack is no longer automatic, though it hasn't been removed — next dev --webpack and next build --webpack remain available for projects with loader configurations that haven't been ported yet.
The practical risk is diagnostic, not architectural. If a team migrates routing and bundler simultaneously and a build fails, there's no way to know from the error alone whether a Server Component boundary was drawn wrong or a webpack loader has no Turbopack equivalent. The more reliable sequence is to first get the app running on Next.js 16 with the --webpack flag, confirm a clean build against the known-good bundler, and only then drop the flag to isolate Turbopack-specific breakage from routing breakage. Module not found errors that appear after removing the flag are almost always a loader compatibility gap rather than a routing bug — worth checking against the Turbopack compatibility table before assuming the migration itself is at fault (https://nextjs.org/docs/app/api-reference/turbopack).
Build request
│
▼
next build (Next.js 16)
│
├── --webpack flag present ──► webpack pipeline ──► known-good baseline
│
└── no flag (default) ───────► Turbopack pipeline
│
├── loader incompatible ──► "Module not found"
└── loader compatible ────► build succeeds
Reported build-time improvements are large enough to justify eventually dropping the flag: Vercel's own figures put production builds at roughly 2 to 5 times faster under Turbopack, and independent teams report comparable results in practice — one e-commerce migration saw build times drop from around 180 seconds under webpack to about 45 seconds after the switch. Development-mode Fast Refresh shows the larger jump, with some benchmarks reporting up to 10 times faster hot-reload cycles. None of that is a reason to adopt Turbopack and the App Router in the same commit.
Cache Components Change What "It Works in Production" Means
This is the change most likely to produce an incident that looks nothing like a routing bug. Next.js 13 through 15 cached App Router output implicitly, in ways that regularly confused developers around revalidation timing. Next.js 16 introduces Cache Components, built on Partial Pre-Rendering and the use cache directive, and flips the default: rendering is dynamic unless a route explicitly opts into caching.
For a route being migrated straight out of the Pages Router — where getServerSideProps made the per-request cost explicit and predictable — that flip can be invisible functionally and expensive operationally. The migrated route still returns correct output; it just does more server work per request than the old implementation did, because nothing was ever marked cacheable under the new explicit model. That's not a bug a functional test catches. It's a latency and infrastructure-cost regression that shows up in monitoring days after the route went live, at which point it gets misattributed to traffic growth or unrelated backend load. Load-testing each migrated route against its old Pages Router baseline — not just verifying it renders the right HTML — is the check that catches this before it becomes an incident.
A Sequencing Order That Keeps the Blast Radius Small
Pulling the prerequisites together, a rollout that limits how much can go wrong in a single deploy looks like: Node and React upgraded and shipped as their own release first; then the root layout shell built in app/ and deployed alongside the still-functioning pages/ tree; then a single low-traffic leaf route migrated and left running in production for days to weeks to validate data fetching and auth patterns under real load; then the remaining leaf routes; then dynamic segments; then API routes converted to Route Handlers last, once everything upstream of them is stable; and only after all of that, proxy.ts and the Turbopack default, each verified in isolation as described above.
Key Takeaways
- The precedence rule (
app/overrides matchingpages/routes) is what makes incremental migration possible — but it only covers routing, not the runtime, caching, or bundler changes bundled into the same release. - Node 20.9+ and React 19.2 are hard floors for Next.js 16; verify CI and container images against them before touching any router code.
- The official codemod converts async request APIs and config renames automatically, but shared components using
next/routeror pages-only i18n providers need manual, often block-level, migration. proxy.tsmoves request interception from the Edge Runtime to the Node.js runtime — re-verify auth and session logic there independently of the page migration.- Cache Components make caching opt-in by default; a migrated route that isn't explicitly marked cacheable can silently cost more per request than its Pages Router predecessor did.





