Next.js 16 "use cache" Errors: Why Cache Components Break
Next.js 16 replaced implicit caching with the explicit "use cache" directive. Here is what actually breaks when you flip cacheComponents on, and how to fix it without reverting.
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.
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading
Next.js Stale Data: How I Fixed Cache Revalidation
My mutations worked but the UI showed stale data. Took me a week to understand Next.js App Router caching. Here are the 6 fixes that made my data fresh again.
Next.js 15 Caching Explained: Why Data Shows Stale
Upgraded to Next.js 15 and suddenly your data is stale — or refreshing too often? The caching model changed completely. Here is what actually happens and how to control it.
Next.js 16 /_not-found Prerender Build Error: The Real Fix
Next.js 16 build crashes prerendering the internal /_not-found route. Here are the three actual causes behind it, how to tell which one is yours, and the fix for each.
Browse by Topic
Find stories that matter to you.
