The Next.js 15 to 16 migration is one of the more substantive upgrades in the App Router era — Cache Components reshape how caching is reasoned about, the use cache directive replaces unstable_cache, and middleware.ts is renamed to proxy.ts. The good news: the renames are codemoddable, unstable_cache keeps working while you decide what to move, and Cache Components is opt-in, so a phased rollout can move one route at a time. How long that takes depends on how many cached call sites and routes your app has, which is what the audit phase below measures.
What changed and why it matters. Next.js 16 ships five distinct axes of change at once: a new caching model (Cache Components, an opt-in flag that brings Partial Prerendering and the use cache directive under one setting), the directive itself with companion helpers (cacheLife, cacheTag, updateTag), an optional migration path off unstable_cache, a file rename from middleware.ts to proxy.ts, and runtime changes (Turbopack as the default bundler, and proxy.ts running on the Node.js runtime only). Each has its own migration step; together they need a plan.
What this playbook covers: a phase-by-phase rollout from the audit step through codemod application, opt-in cache adoption, and post-migration cleanup. Concrete codemods Vercel ships and what they cover, what they leave untouched, the patterns that can bite in production (over-broad cache invalidation among them), and the rollback procedure for the first 24 hours after the bump lands.
Support note, October 22, 2026: the Next.js support policy keeps each major in Maintenance LTS for two years after its initial release. Next.js 15 was released October 21, 2024, so it left Maintenance LTS on October 21, 2026; the policy page still listed 15.x as Maintenance LTS when read that day. Our Next.js 15 end-of-support guide sets out the upgrade options.
- 01Cache Components is opt-in — adopt it gradually.Turn on the cacheComponents flag, then work route by route, not big-bang. Start with a stable, high-traffic page where you can watch cache behaviour and roll back cleanly if invalidation behaves unexpectedly.
- 02unstable_cache keeps working; migrating it is optional.No official codemod rewrites it to use cache, so any conversion is manual or scripted, and closures over request-scoped data, header-derived keys and conditional caching need review. Keep unstable_cache where values must survive a deploy: use cache entries do not.
- 03proxy.ts is a rename, with one runtime catch.The middleware-to-proxy codemod renames the file, the middleware export and the related config keys. proxy.ts runs only on the Node.js runtime; a file that must stay on Edge keeps the deprecated middleware.ts name for now.
- 04Scope cache tags to the narrowest entity.Be specific with cacheTag. A single 'home' tag on every cached function means one invalidation expires everything; scope tags to the narrowest entity that actually changed.
- 05Read request-time APIs outside the cached scope.Under Cache Components, cookies(), headers() and searchParams cannot be read inside a use cache function or any helper it calls. Read them outside and pass the values in as arguments.
01 — What's New16 ships in five axes — Cache, use cache, proxy, codemods, runtime.
Next.js 16 is not a single-feature release. The major-version bump consolidates a year of work across five distinct axes, each of which has its own migration implications. Reading the release as a checklist of five independent migrations rather than one monolithic upgrade is the right mental model.
The five axes, in the order they typically affect a migration: the caching model shifts from the legacy fetch-cache plus revalidate approach to Cache Components — an opt-in cacheComponents flag that enables Partial Prerendering, the use cache directive for explicit cached functions, and tag-based invalidation via cacheTag with revalidateTag and the new updateTag. The docs say unstable_cache has been replaced by use cache, but it keeps working under Cache Components, migrating it is optional, and no official codemod performs the rewrite. The middleware file is renamed to proxy.ts at the filesystem level — same primitive, new name. Official codemods handle the mechanical renames: middleware-to-proxy, remove-unstable-prefix and remove-experimental-ppr. Turbopack is the default bundler, and proxy.ts runs on the Node.js runtime; the Edge runtime stays available only through the deprecated middleware.ts. Alongside these five axes, the same release also stabilizes automatic memoization with the React Compiler, which removes most hand-written useMemo and useCallback from the client tree.
Cache Components
The headline change, enabled with cacheComponents: true in next.config. It turns on Partial Prerendering; the use cache directive marks functions for caching, and cacheLife and cacheTag control lifetime and invalidation. Replaces the implicit caching of earlier App Router versions.
unstable_cache stays
The docs mark unstable_cache as replaced by use cache, but it keeps working under Cache Components and migrating is optional. No official codemod rewrites it. Unlike use cache, it persists cached values across deployments.
middleware → proxy
middleware.ts becomes proxy.ts. Same logic, same matcher config. Next.js says the rename clarifies the network boundary; proxy.ts runs on the Node.js runtime only. Codemod handles the file move.
Turbopack + Node.js proxy
Turbopack is the default for both dev and build; the release post cites 2–5× faster production builds and up to 10× faster Fast Refresh. proxy.ts runs on the Node.js runtime, which cannot be configured; keep middleware.ts (deprecated) for anything that must stay on Edge.
use cache directive changes what is cached and for how long; proxy.ts runs only in Node.js, which widens what it can import but moves Edge middleware onto a different runtime. Plan the migration end-to-end before you start — do not codemod first and reason later.The release adopts a clear philosophy: caching becomes explicit and composable rather than implicit and global. Next.js 13 and 14 cached fetch calls by default and required opt-outs via { cache: 'no-store' } or revalidate: 0; Next.js 15 stopped caching fetch requests, GET Route Handlers and client navigations by default; and with Cache Components enabled, Next.js 16 caches nothing unless you mark it with use cache. That makes it easier to see why a page is or is not cached, but it means every existing app needs an audit of where caching was being relied on implicitly.
02 — Cache ComponentsPPR + use cache + cacheLife + cacheTag.
Cache Components is the umbrella name for the new caching model. The four pieces work together. Partial Prerendering (PPR) splits a page into a static shell (prerendered at build) and dynamic holes (rendered at request and streamed in). The use cache directive marks a function as cacheable — its return value is cached on first invocation and reused on subsequent calls that match the same arguments. cacheLife sets the lifetime profile for a cached function; the presets are default, seconds, minutes, hours, days, weeks and max. cacheTag attaches one or more tags to a cache entry. updateTag, which works only inside Server Actions, expires every entry carrying a tag and reads fresh data in the same request; revalidateTag, which now takes a cacheLife profile as its second argument, marks tagged entries stale and also works in Route Handlers.
The four primitives, in plain English
Three lines of code. use cache at the top of a function says "memoize my return value across requests." cacheLife('hours') says "keep that memoization for the hours profile." cacheTag(`product:${id}`) says "associate this entry with the product:42 tag so a future updateTag('product:42') wipes it." That is the entire surface area — everything else is which functions to cache and how to scope tags.
The shift from revalidate (a time-based interval) to cacheLife profiles is a quiet but important simplification. Instead of every cache call site picking a number of seconds, you choose from a small set of named profiles (default, seconds, minutes, hours, days, weeks and max), and each profile sets three values: stale, revalidate and expire. Centralizing the profiles in configuration means a single change tightens or loosens caching across the entire app — useful during incident response and for consistency across a team.
Tag-based invalidation via cacheTag and updateTag is the right model for any app with a non-trivial data graph. revalidatePath remains available, and revalidateTag was updated rather than replaced: it now requires a cacheLife profile as its second argument (the single-argument form is deprecated), while updateTag covers read-your-writes in Server Actions. The discipline rule for tags is covered in Section 07: scope tags as narrowly as the entity that actually changes, never broader.
Unlike the implicit caching found in previous versions of the App Router, caching with Cache Components is entirely opt-in.Jimmy Lai, Josh Story, Tim Neutkens and Sebastian Markbåge, Next.js 16 release post, Next.js blog, October 21, 2025
One implementation note worth pinning down: use cache can be applied at three scopes — file, function, or component. File-level "use cache" at the top marks every export as cached; function-level wraps an individual async function; component-level wraps a Server Component. Start at function scope when migrating; the granular scope is easier to reason about and easier to roll back if a particular cache key misbehaves. File-scope and component-scope are sensible consolidations once a page is stable.
03 — unstable_cacheThe optional migration path.
unstable_cache was the workaround that many teams adopted in Next.js 14 and 15 to memoize non-fetch data calls — database queries, third-party SDK responses, computed values. In 16, the API reference says it has been replaced by the use cache directive and recommends the switch, but the Migrating to Cache Components guide is explicit: “Your existing fetch and unstable_cache caching keeps working. Migrating unstable_cache is optional and can happen later.” Vercel's official 16 codemods cover the file rename ( middleware-to-proxy), experimental_ppr removal, and the unstable_-prefix drop on stabilized cache APIs ( remove-unstable-prefix) — there is no official unstable_cache → use cache codemod, so the conversion below is a manual or jscodeshift-assisted rewrite you script per repo.
What scripts a clean conversion
The straightforward case — a top-level async function wrapped in unstable_cache with a static key array — converts to an async function with "use cache" at the top, with the cache key options translated to cacheLife and cacheTag calls inside the function body. The guide notes that the cache key is derived from the arguments, so the key-parts array is no longer needed. A short jscodeshift transform can recognize these shapes.
What needs manual review
Three patterns any script should surface as TODO comments rather than converting in place. First, request-time reads and closures — a cookies(), headers() or searchParams read inside the wrapped function, or in any helper it calls, fails under use cache, so move the read outside and pass the value in. Closed-over variables behave differently too: the unstable_cache reference says closures must be added to the key-parts array by hand, while the use cache reference says they are captured into the cache key automatically. Second, conditional caching — code that wraps unstable_cache only when a condition holds needs to be re-expressed; use cache is always-on once applied. Third, nested caching — a cached function calling another cached function works under use cache, but check that each level's key still includes what should split the cache.
When to leave it in place
One behavioural difference argues for leaving some call sites alone. The migration guide states: “Like the fetch Data Cache, unstable_cache persists cached values across deployments and serverless instances, while use cache does not.” The use cache reference adds that neither caching directive carries over to a new deploy, because the cache key includes the build ID, and points data that needs to persist across deploys to unstable_cache or the fetch cache. A call site whose value is expensive to recompute after every deployment is a reason to keep unstable_cache, not a migration backlog item.
For teams that choose to migrate a large surface area of cached calls, a practical workflow is: script the static-key conversion, audit every flagged call site in a single pass, and ship the converted code behind a feature flag if the cache behavior is critical. The use cache directive was introduced as an experimental feature in 15.0 and stabilized in 16.0, so rewrites landed on 15 first run on the experimental version of the directive.
unstable_cache wrapper that closes over a tenant or user ID without adding it to the key parts can already serve one tenant's cached result to another, a cross-tenant data leak. Treat the audit as a security review, not a refactor.04 — proxy.tsmiddleware.ts becomes proxy.ts.
The simplest of the five axes, with one catch. middleware.ts is renamed to proxy.ts. Next.js says the rename clarifies the app's network boundary and routing focus, and the release post's instruction is short: rename the file and the exported function to proxy, and "Logic stays the same." The catch is the runtime, covered below.
What the codemod does
The middleware-to-proxy codemod (see Next.js codemods reference) renames middleware.ts to proxy.ts, renames the middleware named export to proxy, and rewrites the related next.config keys ( experimental.middlewarePrefetch → experimental.proxyPrefetch, skipMiddlewareUrlNormalize → skipProxyUrlNormalize, and two more experimental keys). The matcher config is unchanged.
What changed beyond the name
One material change: proxy.ts runs on the Node.js runtime, and the version 16 upgrade guide says that runtime cannot be configured: the Edge runtime is not supported in proxy. A file that must stay on Edge keeps the middleware.ts name, which still works in 16 but is deprecated and will be removed in a future version. The upside of Node.js is that the proxy can call a library that does not run on Edge (a heavyweight SDK, a database client, a crypto library with native bindings) instead of handing that work to a downstream route handler.
Practical rule of thumb: before running the codemod, decide whether each piece of middleware moves to proxy.ts on Node.js, where you should re-test its latency, or stays on Edge as middleware.ts, which is a stopgap because that name is deprecated. If your middleware was working around an Edge limitation by calling a separate route handler, proxy.ts on Node.js can collapse that indirection.
05 — CodemodsWhat Vercel ships and what they cover.
Vercel ships codemods through the @next/codemod CLI. Running npx @next/codemod@latest opens an interactive transform picker; running npx @next/codemod@latest upgrade major attempts the full sweep when bumping across a major (see the codemods reference for the patch/minor/major/ specific-version revisions). The codemods cover the mechanical changes — file renames, API signature shifts, import path updates, and the unstable_-prefix drop on stabilized cache APIs — and leave the conceptual decisions (which functions to cache, how to scope tags, what to put behind PPR) to the team.
The matrix below summarizes what each codemod covers
The order shown is a suggested sequence: the request-API sweep first, because synchronous access is a breaking removal in 16 and the upgrade guide says the upgrade command does not run that codemod for you, then the renames, then the manual steps.
next-async-request-api sweep
Converts synchronous cookies(), headers() and draftMode() calls to their async forms, and flags what it cannot convert. Next.js 16 removed the synchronous compatibility that 15 allowed, including for params and searchParams; if you already ran this on a 15 codebase, skip.
middleware-to-proxy
Renames middleware.ts to proxy.ts, renames the middleware export to proxy, and rewrites the related next.config keys (experimental.middlewarePrefetch → proxyPrefetch, skipMiddlewareUrlNormalize → skipProxyUrlNormalize). Mechanical, but check first whether the file must stay on Edge.
remove-unstable-prefix
Drops the unstable_ prefix from stabilized APIs. The codemods page's example turns import { unstable_cacheTag as cacheTag } from 'next/cache' into import { cacheTag } from 'next/cache'.
manual: unstable_cache → use cache
No official codemod exists for this rewrite. A per-repo jscodeshift transform can handle the static-key shape and emit TODO comments for request-time reads, closures, conditionals and nested caches. Audit every TODO; anything that decides whose data is cached is security-sensitive.
manual: PPR opt-in per page
There is no codemod for adopting Partial Prerendering route by route. With cacheComponents enabled, the migration guide lets you opt routes out of validation with instant = false and convert them one at a time. Pick high-traffic, stable pages first.
What the codemods deliberately do not cover: any decision that requires understanding the data model. Cache-tag scope, cache-life profile selection, which functions deserve use cache at all, where to apply PPR — these all require thinking about how data flows through the app. The codemods clear the mechanical debt so the team can focus on the substantive decisions; they do not replace the substantive decisions.
The other thing worth knowing: the codemods are not perfect. Always run them on a clean working tree, review the diff before merging, and add a regression-test pass before pushing to production. For larger apps, run the codemods on a long-lived branch and let the team exercise it before merging.
06 — Phased RolloutAudit → codemod → opt-in cache → cleanup.
The four-phase rollout below separates the work by risk. Each phase has a distinct purpose and its own rollback, so compressing two phases into one removes a rollback point.
Audit & inventory
Inventory every cache call site (unstable_cache, fetch with revalidate, route segment config). Map the data graph — which entities invalidate together, which mutations affect which pages. Document the existing cache-tag taxonomy if one exists.
Run codemods
Run the codemods in order on a long-lived branch. Audit every TODO. Run the full build and full test suite locally. Deploy to a preview environment and exercise the critical user paths. Do not merge until preview is green.
Opt-in cache per page
Enable cacheComponents and adopt use cache on one stable, high-traffic page first. Watch its cache behaviour and error logs for a day. If clean, roll forward to the next page. This is the phase where behaviour changes reach production, so it gets the most care.
Cleanup
Once every targeted page is on the new model, remove the unstable_cache imports you converted (keep any whose values must persist across deployments), consolidate cacheLife profiles, document the tag taxonomy in the repo. Final lint + type-check pass to catch any drift the codemods left behind.
The discipline that holds the whole rollout together: do not skip Phase 1. The audit step is where you discover the call site depending on an implicit cache that nobody documented, the route that quietly expects revalidate: 0, the third-party integration that breaks when its responses get cached. Skipping the audit and discovering these in production is the most expensive way to find them.
Measurement is the discipline that holds Phase 3 together. Use whatever cache metrics your hosting platform or cache handler exposes, and watch them on the first page you migrate before rolling to the second. A hit rate far below what you expected points to a cache-key problem that will compound if you replicate the pattern. The first page is the one where you tune the model; subsequent pages should be roll-outs of a known-good pattern, not re-discoveries.
use cache and cacheLife; the rest of the app continues to run on 16. Phase 4: re-add anything that was prematurely deleted. Never deploy a phase without confirming the rollback procedure for that phase in advance.For teams with multiple environments, a cautious cadence is: Phase 2 soaks in staging before it merges, Phase 3 rolls out one page at a time to production with a soak between pages, and Phase 4 follows once the last Phase 3 page is stable. For teams with no preview infrastructure, set one up before starting; a migration of this scope is hard to test safely without one.
If your team has not migrated to App Router yet, that work precedes this playbook. Our web development engagements include App Router migrations and Next.js major-version bumps as a packaged offering — the Pages Router to App Router move is a larger lift than this 15 to 16 step, but the playbook shape is similar.
07 — Common PitfallsFour cache-related failure modes.
The four pitfalls below follow from how the Cache Components APIs are documented. Reading them first is cheaper than finding them in production. Each one has a clean fix; the risk is not knowing to look for it.
Over-broad cache invalidation
A single 'home' or 'app' tag on every cached function. The first updateTag('home') call expires every entry carrying it. Scope tags to the narrowest entity that actually changed — product:id, user:id, order:id — never broader.
Build vs request confusion
A function marked use cache that calls cookies() or headers(), directly or through a helper, fails with an error. The use cache reference warns that on a dynamically rendered route this can pass next build and fail under next start. Move the read outside the cached function and pass the value in.
Keys that miss the tenant
An unstable_cache wrapper that closes over a tenant key without adding it to its key parts, or any cached function that reads a tenant from shared module state, can serve one tenant's output to another. use cache captures closures into the key, but pass the identifier as an argument so the key is explicit.
cacheLife profile mismatch
Using cacheLife('weeks') on data that mutates daily, or cacheLife('seconds') on data that almost never changes. The first wastes correctness, the second wastes the cache. Centralize the profile catalog and apply consistently per data class.
updateTag() is a new Server Actions-only API that provides read-your-writes semantics, expiring and immediately reading fresh data within the same request.Jimmy Lai, Josh Story, Tim Neutkens and Sebastian Markbåge, Next.js 16 release post, Next.js blog, October 21, 2025
One rule covers Pitfalls 02 and 03: read request-time values outside the cached scope and pass the identifiers that should split the cache in as arguments. The use cache reference says closed-over variables are captured into the key automatically and that cookies(), headers() and searchParams cannot be read inside a cached function; the unstable_cache reference says closures must be added to the key parts by hand. No official codemod converts unstable_cache, so audit existing call sites for that omission and have a conversion script flag them rather than rewrite them.
For teams who want to go deeper on the stack-level decisions before starting a migration like this, the Next.js 16 AI chatbot tutorial covers the streaming and route-handler patterns that interact with the cache model; the Vercel AI SDK v5 to v6 migration playbook walks through a parallel-shaped upgrade if your app also depends on the AI SDK.
A final note on observability. Once Cache Components is enabled, alert on whatever cache metrics your platform exposes when a route drops below its expected hit rate. A sudden drop usually points to a cache-key change or an over-eager updateTag or revalidateTag call site, and the earlier you see it, the smaller the fix. Invalidation counts, where your platform or cache handler records them, are the place to look when Pitfall 01 is suspected.
Next.js 16 is a meaningful upgrade: codemod the renames, move unstable_cache only where it pays, and adopt in phases.
The 15 to 16 migration is a meaningful upgrade — five axes of change, a new caching model, a literal file rename, and a material shift in defaults. None of the individual pieces is exotic, but the interaction surface is large enough that a disciplined rollout is worth planning before the first codemod runs. The four-phase shape — audit, codemod, opt-in cache per page, cleanup — keeps every step reversible, and the audit tells you how long the rest will take.
The codemods carry the mechanical load. next-async-request-api sweeps the synchronous request-API reads, middleware-to-proxy moves the middleware file, and remove-unstable-prefix renames the stabilized cache APIs. unstable_cache has no codemod and needs none to keep working: convert it by hand where use cache fits, and leave it where cached values must survive a deploy. What the codemods cannot do is the substantive caching decisions — which functions to mark, how to scope tags, what to put behind PPR, which cacheLife profile fits each data class. Those are human decisions and the right place to spend the team's time.
The broader signal: Next.js is moving from an implicit-cache framework to an explicit-cache one. A team that adopts the new model gains a clearer mental picture of where their data comes from, how long it persists, and what triggers its invalidation — and that clarity is the foundation for every higher-order optimization a Next.js app eventually wants (per-tenant cache, regional cache, durable cache backed by a specific store). The migration is the ticket; the long-term payoff is being able to reason about caching at all.