Next.js 16 flipped the caching model. Through Next.js 15, pages and fetches were cached by default and you opted out. With cacheComponents: true in Next.js 16, nothing is cached by default and you opt in with the 'use cache' directive. That's a bigger behavioral change than the version bump suggests, and it produces a specific, recurring family of build and runtime errors that generic "here's what use cache does" tutorials don't cover, because they're written against a fresh demo app, not a real one with i18n, auth, and third-party SDKs already wired in.
This is what actually breaks, and how to fix each case, based on the current cacheComponents docs and reports like amannn/next-intl#2229.
First: confirm you're actually opted in
'use cache' only does anything once cacheComponents: true is set in next.config.ts. If you added the directive but nothing changed, that's the first thing to check — it's a silent no-op without the flag, which itself causes confusion ("I added use cache and nothing happened").
// next.config.ts
const nextConfig = {
cacheComponents: true,
}
Error 1 — "headers() / cookies() cannot be called inside 'use cache'"
This is the single most common failure once you enable cacheComponents. It happens whenever a function marked 'use cache' — directly, or indirectly through something it calls — reads headers(), cookies(), or any other request-scoped API. The confirmed next-intl case (#2229) is a textbook example: getTranslations() reads locale information from headers() internally, so wrapping a component that calls it in 'use cache' throws even though you passed a locale prop explicitly — the library ignores your prop and reaches into the request anyway.
Fix: cached functions must be pure with respect to the request. Move the 'use cache' boundary below anything that reads request state — cache the pure data-fetching function, not the component that also renders user-specific chrome. If a third-party library reads request context internally (i18n, auth SDKs, geolocation), don't wrap components that use it in 'use cache' at all; cache only the deterministic parts around it.
Error 2 — cached value never updates (missing cacheLife)
If your cached function runs once and then never refreshes, you're missing cacheLife. Without it, Next.js applies a default profile that may be far more aggressive than you expect for a data-heavy page.
import { unstable_cacheLife as cacheLife } from 'next/cache'
async function getPosts() {
'use cache'
cacheLife('minutes') // stale/revalidate/expire window — pick the built-in profile that matches your data
return db.posts.findMany()
}
cacheLife takes three practical knobs: how long the cache serves stale before checking, when it triggers a background refresh, and the hard expiry before eviction. Skipping it doesn't disable caching — it just applies defaults you didn't choose.
Error 3 — mutation doesn't invalidate the cache
If a user updates data and still sees the old version, you're missing cacheTag plus a matching revalidateTag/updateTag call in the mutation.
async function getProject(id: string) {
'use cache'
cacheTag(`project-${id}`)
return db.projects.findUnique({ where: { id } })
}
// in the Server Action that mutates it:
import { revalidateTag } from 'next/cache'
revalidateTag(`project-${id}`)
Without a tag, there's nothing to invalidate — the cache entry sits until its cacheLife expiry regardless of what your mutation does.
Error 4 — putting 'use cache' on the page itself
Best practice from the current Next.js docs: don't put 'use cache' directly on a page component. Pages are orchestration layers that assemble Server Components, params, and search params — none of which are cache-safe by nature. Put it on the data-fetching function or on a cached child Server Component instead, and let the page stay dynamic around it. Caching the page directly is what produces the confusing "why is my whole page stale/stuck" reports.
If you're migrating from unstable_cache
unstable_cache still works, but it's the old, less granular API. The practical migration path is: replace unstable_cache(fn, keyParts, options) calls with a 'use cache' function plus explicit cacheLife/cacheTag calls — the tag/key logic maps roughly 1:1, but cacheLife profiles replace the old revalidate option with more granular stale/revalidate/expire control.
Related reading
If you're also hitting a /_not-found prerender crash while doing this migration, that's very likely a separate Next.js 16 issue, not this one — see Next.js 16 /_not-found prerender build error: the real fix. For the Next.js 15 caching model this replaces, see Next.js 15 caching, explained and Next.js stale cache / revalidation fix. For the Supabase side of a cached data layer, see Next.js + Supabase caching strategies.
Originally published at https://www.iloveblogs.blog
Top comments (0)