Next.js 16.4 Cache Components: A Migration Guide for Existing Apps
Next.js 16.4 recommends Cache Components for every app; it becomes the default in Next.js 17. The migration order, config mappings, and five gotchas that break real apps.

On October 6, 2026, Next.js 16.4 shipped with a message every team running Next.js needs to take seriously: Cache Components is now the recommended caching model for every app, and it becomes the default in Next.js 17. If you run a Next.js app in production, you are migrating to this model. The only choice is whether you plan it or rush it later. I would rather you plan it. Here is the migration order I would follow on a real app, the exact replacements for every old route config, and the five gotchas that break real migrations.
The short answer
Cache Components moves Next.js caching from route-level inheritance to
component-level opt-in: you mark what may be cached with 'use cache' and
set lifetimes with cacheLife. The safe order is upgrade to Next.js 16,
enable the cacheComponents flag, opt routes out of validation with
instant = false, and convert one route at a time.
What Cache Components actually changes
The old mental model was inheritance. You put dynamic, revalidate, or
fetchCache on a route segment, and everything under it inherited caching
behavior, including the fetch calls that silently defaulted to cached.
That model produced a whole genre of debugging session: "why is this page
stale?" followed by someone discovering a config three layouts up.
The new model is opt-in. Nothing is cached unless you mark it. The
'use cache' directive on a function or component works like a
component-level Cache-Control header, to borrow the comparison from the
16.4 release coverage, and cacheLife sets how long that cached result
lives. Everything else runs at request time by default, and Next.js
prerenders the static or cached UI around it. This is also the model that
finally makes Partial Prerendering practical: a route serves its static
shell instantly and streams the dynamic regions inside <Suspense>
boundaries, instead of the whole page waiting on the slowest query.
Why does this matter to you right now? Two reasons. First, the direction is announced: recommended for every app in 16.4, default in Next.js 17. Second, the migration tooling exists today. There is an official migration guide, an adoption skill for coding agents, and a codemod that performs the mechanical opt-out across your whole app. Migrating incrementally now is dramatically cheaper than migrating under the pressure of a forced major-version upgrade.
nextjs.org
Migrating to Cache Components - the official Next.js docs
If the React-to-browser pipeline still feels fuzzy, my
frontend ecosystem walkthrough maps
the whole thing from npm create to the browser.
Step 1: get to Next.js 16, then flip one flag
Cache Components requires Next.js 16. If you are on 15 or earlier, work through the version 16 upgrade guides first. The migration guide is explicit about this, and skipping it just stacks two migrations into one painful session.
Once you are on 16, the flag itself is one line in next.config.ts:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfigThat flag replaces the old experiments: if you were using
experimental.dynamicIO or experimental.useCache, cacheComponents is
their successor. New apps scaffolded with create-next-app get the model
enabled by default, and 16.4 ships with React 19.3.
One piece of practical advice: do not flip this flag on a Friday afternoon. The moment it is on, Next.js starts validating every route for instant navigation in development, and route segments that still export the old configs will error. That is the point, the validation is the migration driving you, but you want a calm morning and a green test suite when it starts.
Step 2: choose incremental or direct adoption
For a production app, choose incremental. Direct conversion is for small apps and greenfield projects where you can see every route at once.
The official path uses the next-cache-components-adoption skill, which
drives the migration with a coding agent one feature at a time. Install it
with:
npx skills add vercel/next.js --skill next-cache-components-adoptionThe skill supports two modes. Incremental opens one mechanical PR that
opts every route out of validation, then converts features in follow-up
PRs. Direct converts every route in place on one branch. There is also
next upgrade --agent, which hands your coding agent version-specific
upgrade guidance, so the agent stops guessing and follows the actual
migration rules.
The opt-out mechanism is a route segment config called instant:
export const instant = falseThis marks a segment as allowed to block instant navigation, which defers its validation until you are ready. To apply it to the whole app in one pass, run the codemod:
npx @next/codemod@canary cache-components-instant-false ./appStep 3: replace every route config (the full mapping)
With the flag on, route segments that still export dynamic, revalidate,
or fetchCache error. Here is what each one becomes:
| Old config | What it did | Cache Components replacement |
|---|---|---|
dynamic = 'force-dynamic' | Rendered the whole route per request; uncached fetch | Just remove it. Uncached data and request-time APIs already run at request time. |
dynamic = 'force-static' | Forced prerender; made cookies(), headers(), useSearchParams() return empty values | Remove it, mark cacheable work with 'use cache' + cacheLife('max'), and add ensureStatic = 'navigation' only if the route must stay static |
dynamic = 'error' | Errored when prerendering was blocked; treated fetch as cached by default | Remove it. Wrap the fetches you want cached in 'use cache' + cacheLife; after removal, fetch no longer inherits a cached default |
export const revalidate = N | Route-level revalidation time | cacheLife profiles: 'seconds', 'minutes', 'hours', 'days', 'weeks', 'max', or a custom profile you define |
fetchCache | Fetch-level caching config | 'use cache' with cacheTag() and cacheLife() on the data that needs it |
unstable_cache() | Manual cache utility with key arrays and tag options | The 'use cache' directive. Tags move into cacheTag() calls, revalidation into cacheLife(). Migrating is optional and can happen later |
The force-static conversion is the one you will do most often. Here is the
before and after:
Before: caching inherited from the route segment. Request-time APIs silently returned empty values to keep the route prerenderable.
export const dynamic = 'force-static'
export default async function Page() {
const data = await fetch('https://api.example.com/data')
return <div>...</div>
}After: caching declared exactly where the data is fetched. Request-time APIs read real request values again, so remove those dependencies if the route must remain fully prerendered.
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('max')
const data = await fetch('https://api.example.com/data')
return <div>...</div>
}For revalidate, if your old value does not match a built-in cacheLife
profile, pick the closest one or define a custom profile with your own
timings. You can even redefine a built-in profile, including default,
when its timings do not match your conventions.
Step 4: enforce static output with ensureStatic (new in 16.4)
This is the genuinely new piece in 16.4, and it deserves its own step because it is not just a renamed config. To require a completely static route after migrating its caching, add:
export const ensureStatic = 'navigation'The difference from dynamic = 'force-static' is the whole point.
force-static made cookies(), headers(), and useSearchParams()
return empty values so the route could keep prerendering. It hid the
problem. ensureStatic does the opposite: if request-specific server work
prevents the route from being prerendered, you get an error at build time
instead of silently wrong output.
Add it only where a route has a genuine static-output requirement after the migration: marketing pages, docs, SEO-critical landing pages. If only the app shell or per-link prefetch needs to stay static, choose a less restrictive level. On a dashboard that is dynamic by nature, skip it entirely. The docs call this step optional, and I agree. Enforcement is for routes where staleness is a bug, not a feature.
The five gotchas that break real migrations
The flag flip is the easy part. Migrations fail on these five, and every one of them is documented, which means every one of them is avoidable.
1. Request-time APIs inside 'use cache' throw. You cannot call
cookies(), headers(), or read searchParams inside a 'use cache'
block. It makes sense once you say it out loud: cached output has to be
identical for every request, and those APIs are per-request. The fix is to
read the dynamic value outside the cached function and pass it in as an
argument, where it automatically becomes part of the cache key:
// Wrong: runtime API inside use cache
async function CachedProfile() {
'use cache'
const session = (await cookies()).get('session')?.value // Error
return <div>{session}</div>
}
// Right: read outside, pass in as an argument
async function ProfilePage() {
const session = (await cookies()).get('session')?.value
return <CachedProfile sessionId={session} />
}
async function CachedProfile({ sessionId }: { sessionId: string }) {
'use cache'
const data = await fetchUserData(sessionId) // sessionId is part of the key
return <div>{data.name}</div>
}If you genuinely cannot refactor, 'use cache: private' is the escape
hatch: it permits runtime APIs but caches per-user, not shared. Use it
sparingly. It is not a shared cache.
2. Synchronous database drivers complete during prerender. Queries to
embedded databases with synchronous APIs, like better-sqlite3 or
Node's built-in node:sqlite, finish during prerendering under Cache
Components. If the answer must be live, call await connection() before
the query so it runs at request time. This one bites hardest on apps that
moved from a hosted database to SQLite for cost reasons.
3. try/catch swallows the prerender bail-out. Under Cache Components,
reading uncached or runtime data bails out of prerendering by throwing. If
your code wraps data fetching in a try/catch that logs, you will get noisy
build output instead of a clean signal. When your build logs suddenly fill
up after the flag flip, check your error boundaries and catch blocks before
blaming the framework.
4. Route Handler caching has its own rules. Non-GET methods are never
cached, in any version or model, and a cached GET does not know about
writes from elsewhere, so invalidate from the POST handler or webhook
that performs the write. Under Cache Components, pass an explicit
cache-life profile when revalidating. Two deprecations ride along here:
the single-argument revalidateTag form is deprecated in 16, and
export const runtime = "edge" is deprecated too, because Cache Components
requires the Node.js runtime.
5. Metadata routes kept the old default. Special route handlers like
sitemap.ts, opengraph-image.tsx, and icon.tsx remain static by
default unless they use request-time APIs or dynamic config options. Most
of the time this means you can leave them alone, which is a relief, but do
not "fix" them preemptively and introduce dynamic behavior where none
existed.
Should you migrate now, or wait for Next.js 17?
If you are on Next.js 16, start the incremental migration now. If you are on 15 or mid-launch, wait, but put it on the roadmap, because the default flip in 17 is announced and the tooling is best right now.
Here is how I would decide it in practice. Start now if you are already on 16, if your app has routes where "why is this stale" is a recurring support ticket (those routes are quietly slow today, and the validation errors will find them), or if you want the agent-assisted tooling while it is fresh and documented. Wait if you are mid-release with a freeze in place, if the app is small enough that a direct conversion later is a day of work, or if you still rely on the Edge Runtime, which you will need to move off first.
My honest recommendation: run the flag flip plus the instant = false
sweep this month, convert your highest-traffic routes first, and let the
adoption skill do the mechanical parts. You get the validation signal
across the whole app immediately, and the actual conversion work lands in
small, reviewable PRs. That is also exactly the engagement shape clients
pay for, which brings me to the last section.
Tools I would use on this migration
A migration is a change with a blast radius, so I instrument it like one. The table below lists the tools I would reach for, with honest one-line reasons. The links are the official sites; I have marked the spots where affiliate links will slot in later rather than pretending they exist now.
| Tool | Why it earns a place in this migration |
|---|---|
| Vercel | Preview deployments for every conversion PR, so you can compare the migrated route against production before merging. Cache Components works out of the box on the platform that ships Next.js. |
| Sentry | The migration changes build and runtime behavior. Error tracking catches the new failure modes (bail-out noise, cache-key mistakes) before your users do. |
| Upstash | If you outgrow the default cache, serverless Redis gives you a shared cache layer without managing infrastructure. Relevant for the cacheLife profiles that span regions. |
| Better Stack | Synthetic checks that your static shell still renders instantly after each conversion. The validation overlay tells you about dev; monitors tell you about production. |
FAQ
Do I have to migrate to Cache Components right now?
No, but you should start soon if you are on Next.js 16. Vercel recommends
Cache Components for every app as of 16.4 and plans to make it the default
in Next.js 17. The incremental adoption mode lets you enable the flag, opt
routes out of validation with instant = false, and convert one route at a
time, so there is no big-bang rewrite.
What happens to my existing dynamic and revalidate configs when I enable the flag?
They stop working as you know them. Route segments that still export
dynamic, revalidate, or fetchCache will error once cacheComponents
is true. The official migration replaces them with the 'use cache'
directive and cacheLife lifetimes, and the dev overlay's validation
errors point at each spot that needs converting.
Does enabling Cache Components automatically make my app faster?
No. The flag changes the caching model, not your caching coverage. Your
app gets faster only when you mark cacheable work with 'use cache' and
set sensible cacheLife lifetimes, and wrap genuinely dynamic work in
<Suspense> so it streams instead of blocking. The instant-navigation
validation shows you exactly which routes still block. If you want to know
which metrics that validation is really protecting, my
performance-is-perception post breaks down
the ones that map to how a UI feels.
What is the difference between ensureStatic and dynamic = 'force-static'?
ensureStatic (new in Next.js 16.4) enforces static output at build time:
if request-specific server work prevents the route from being prerendered,
you get an error instead of silently degraded output.
dynamic = 'force-static' instead made cookies(), headers(), and
useSearchParams() return empty values. ensureStatic reports the
problem; force-static hid it.
Can I migrate one route at a time?
Yes, that is the recommended path for production apps. Set
export const instant = false on routes that are not ready, which defers
validation for them, then remove the opt-out route by route and resolve
each route's insights. A codemod
(npx @next/codemod@canary cache-components-instant-false ./app) applies
the opt-out to the whole app in one pass.
Will Cache Components break my Route Handlers or metadata routes?
Metadata routes like sitemap.ts, opengraph-image.tsx, and icon.tsx
stay static by default unless they use request-time APIs, so they are
usually fine. Route Handlers follow the same caching model, with two
watch-outs: GET caching behavior changed back in Next.js 15, and non-GET
methods are never cached in any model. Also note
export const runtime = "edge" is deprecated, since Cache Components
requires the Node.js runtime.
Need this migration done without breaking production?
This is exactly the kind of work I take on as a freelance Next.js developer: version upgrades, caching-model migrations, and the performance validation that proves nothing regressed. If your team is staring at a Next.js 16 app and a 17 deadline, reach out through my about page and I will scope the migration route by route, with a green build at every step.

