Cache Components
Keep the project's caching mode unless migration is requested. Confirm the
installed Next.js version and cacheComponents configuration before applying
these patterns.
Read the relevant guide in node_modules/next/dist/docs/ first (bundled since
16.2; in a monorepo, use the app's own next package). Web paths map to files
with numbered folders, so find them by name: .../functions/cacheLife is
01-app/03-api-reference/04-functions/cacheLife.md. Under 01-app/, start
with 02-guides/migrating-to-cache-components.md,
01-getting-started/08-caching.md and 02-guides/instant-navigation.md.
Error pages under /docs/messages are not bundled; next dev/next build
print the fix options inline. Without local docs, use the
official caching docs.
Exact exports and signatures are also in node_modules/next/cache.d.ts.
The goal is a useful prerendered shell with freshness and authorization rules that remain correct after mutations, deploys and client navigation.
Facts older examples get wrong
- Next.js 16 enables the mode with top-level
cacheComponents: true. It replacesexperimental.dynamicIO,experimental.useCacheandexperimental.ppr;cacheLife/cacheTagimport fromnext/cachewithoutunstable_. - With it enabled, segment exports
dynamic,revalidate,fetchCacheanddynamicParamsare errors, and Edge runtime is unsupported.revalidate = NbecomescacheLife(nearest or custom profile) insideuse cache;force-dynamicandfetchCacheare unnecessary; fetchnext: { revalidate, tags }becomescacheLife/cacheTagin ause cachefunction; request data goes under Suspense;dynamicParams = falsebecomesnotFound()for unknown params. - Existing
fetchandunstable_cachecaching still works as a separate layer, so do not mechanically rewrite every read. Its persistence across deployments and instances depends on retained/shared storage; self-hosted instances do not share it by default.use cacheis in-memory by default; even a durable handler cannot reuse entries when the build/deployment ID changes. generateStaticParamsmust return at least one param:[]fails the build, and removing the export renders the route on every request.revalidate: 0orexpireunder 5 minutes makes a request-time hole instead of prerendered output; of the presets onlysecondsdoes. 16.3 addsstalethresholds (under 30 s: not prerendered; under 5 min: not in the App Shell).- Synchronous
new Date(),Math.random()orcrypto.randomUUID()during prerender is an error: capture it inuse cache, or defer withawait io()(next/cache, 16.3+) orawait connection()(next/server) under Suspense. use cache: privatewas experimental through 16.2. It runs at request time, stays out of the static shell and accepts no custom handler.export const instant(16.3;unstable_instantin 16.2) asks dev/build to validate instant navigation into a segment.instant = falsepermits a blocking route; it does not clear synchronous-IO errors.
For whole-app adoption or instant-navigation work, Next.js publishes workflow
skills (next-cache-components-adoption, next-cache-components-optimizer)
in vercel/next.js/skills; the bundled migration guide covers the same steps.
Shape of one route
app/posts/page.tsx, checked against next 16.3.8 with cacheComponents: true
(tsc --noEmit, next build, the action under next start). @/lib/* is
project code.
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag, updateTag } from 'next/cache'
import { insertPost, listPosts } from '@/lib/posts' // project code
import { requireEditor } from '@/lib/auth' // throws unless the session may edit
async function getPosts() {
'use cache' // shared entry: no cookies/headers/searchParams inside
cacheLife('hours')
cacheTag('posts')
return listPosts()
}
async function Theme() {
const theme = (await cookies()).get('theme')?.value // request-time, outside the cache
return <p>Theme: {theme ?? 'system'}</p>
}
async function addPost(formData: FormData) {
'use server'
await requireEditor()
const title = String(formData.get('title') ?? '').trim()
if (!title) return
await insertPost(title)
updateTag('posts') // Server Actions only; the next read waits for fresh data
}
export default async function Page() {
const posts = await getPosts() // cached, so it can be part of the static shell
return (
<>
<ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
<Suspense fallback={<p>Theme: …</p>}><Theme /></Suspense>
<form action={addPost}><input name="title" /><button>Add</button></form>
</>
)
}
Decide the boundary
- Cache reusable results when the product's freshness and data policy permit. Static content needs no directive just to be static.
- A plain
use cachescope cannot read cookies, headers, searchParams or callconnection(), including indirectly through helpers. Read those outside and pass authorized, serializable values. Account/tenant IDs must participate in the cache key when output depends on them. On a dynamically rendered route, thenext-request-in-use-cacheerror can passnext buildand surface only undernext start. - Keep uncached I/O and request-only content below appropriate Suspense boundaries. Suspense supplies fallback UI; it does not itself make synchronous work dynamic.
- Evaluate private/remote variants against installed docs and deployment handlers. Private caching is not a compliance guarantee; remote caching is not a consistency protocol.
Define freshness and invalidation
Choose lifetimes from actual acceptable staleness. Named profiles can be overridden by the project; read its config before assuming a duration.
Use tags when invalidation must reach the same entity across routes, path invalidation for route-specific output, or expiry where that meets the contract. Authenticate, authorize and validate before mutations. Invalidate only affected cached data:
updateTag: immediate expiry/read-your-writes, Server Actions only.revalidateTag(tag, 'max'): stale-while-revalidate on a later visit.revalidateTag(tag, { expire: 0 }): immediate expiry where a webhook/Route Handler needs it. The one-argument form is deprecated.
These tag APIs can also apply to tagged fetch data; their availability does not by itself mean Cache Components is enabled.
Read for the problem
- API boundaries: keys, serialization, lifetimes, handlers and route APIs.
- Composition: tenant isolation, nested caches, pass-through and dynamic params.
- Troubleshooting: diagnose blocking, stale or inconsistent output.
Run the production build, then exercise cold/direct navigation, client
navigation, the relevant mutation and subsequent read under next start.
Check tenant isolation and multi-instance invalidation when applicable.
A passing build cannot establish those behaviors.