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 'use cache' 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. Per the official constraint on request-time APIs, a function marked 'use cache' — directly, or indirectly through something it calls — cannot access cookies(), headers(), or searchParams; the restriction follows the whole call stack, not just the function you annotated. The confirmed next-intl case (#2229) is a textbook example: the Link component from createNavigation unconditionally reads request configuration through headers() internally, so wrapping a component that uses 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 { 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 was stabilised in Next.js 16.0.0 — import it directly from next/cache. The old unstable_cacheLife alias still works but is deprecated; if you're migrating from Next.js 15, this is one of the renames the v16 upgrade guide calls out.
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 — caching the whole page captures runtime values it shouldn't#
'use cache' directly on a page component is officially supported — the docs call this UI-level caching and demonstrate it as-is. Putting it there isn't the mistake by itself. The actual trap is a page that also destructures or awaits params or searchParams inside that same cached scope: any value captured from an outer scope or passed as an argument automatically becomes part of the cache key, so if that value is the runtime-only params/searchParams promise, the whole page's output gets pinned to whatever resolved first (or fails to prerender at all). That's what produces the confusing "why is my whole page stale/stuck" reports.
Fix: keep the cached scope free of anything request-specific. Read params/searchParams/cookies()/headers() outside the 'use cache' boundary — or further down the tree, inside a <Suspense> boundary — and pass only the resolved, serialisable value in. Cache the data-fetching function or a child Server Component that receives already-resolved arguments, rather than a page that awaits runtime values directly inside its own cached body.
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.
Error 5 — build fails asking for a Suspense boundary you didn't add#
This one is easy to confuse with Error 1, but it's a separate failure with a separate
cause. Error 1 fires when a cached function reads request state. This one fires when
an uncached dynamic API — searchParams, cookies(), headers(), or a fetch with no
cache directive at all — is used directly inside a page or component that isn't wrapped
in a <Suspense> boundary. With cacheComponents: true, Next.js needs to know exactly
which parts of the page are static (prerenderable at build time) and which are dynamic
(must run per-request). A dynamic API with nothing to catch it fails the build instead of
silently falling back, because falling back silently is exactly the ambiguity
cacheComponents was built to remove.
The fix is not to cache the dynamic part — it's to isolate it behind a Suspense boundary so the rest of the page can still prerender:
import { Suspense } from 'react'
export default function Page({ searchParams }) {
return (
<>
<StaticHeader />
<Suspense fallback={<Loading />}>
<DynamicResults searchParams={searchParams} />
</Suspense>
</>
)
}Everything outside the boundary prerenders once at build time; everything inside it
renders per request. This is the same underlying mechanism behind the useSearchParams()
Suspense requirement from earlier Next.js versions — see
useSearchParams Suspense Error: 3 Real Fixes
for the client-side version of the same "wrap the dynamic part" pattern — except under
cacheComponents it also applies to server-side dynamic APIs, and the build fails loudly
instead of just warning.
'use cache' does not work inside Client Components#
'use cache' is a Server-side-only directive, the same family as 'use server'. It can
mark a Server Component, a plain async function that runs on the server, or a Server
Action — anywhere execution happens on the server and the result can be safely reused
across requests. Adding it to a file that also has 'use client' at the top, or to a
function that only ever runs in the browser, does nothing useful: there's no server-side
cache to write into, because a Client Component's code runs on the user's machine, not on
your server.
The failure mode here is quieter than the other four — it's rarely a hard build error,
it's a directive that's simply inert. If you added 'use cache' to a hook or a
component and nothing about its caching behavior changed, check whether that file is
already a Client Component (either it has 'use client' itself, or it's imported into
one and treated as part of the client bundle). The fix is to push the caching boundary
down into a Server Component that the Client Component then receives data from as props,
not to try to cache the client-side function directly. This is the same server/client
data-flow boundary that governs Server Functions inside proxy-protected routes —
server-only capabilities don't cross into client code just because they're imported into
the same file tree.
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.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
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.
Fix Export encountered errors on following paths in Next.js
Read the path list as a map back to your own code: /about/undefined, /components/Navbar and /about/[slug] each point at a different bug, rebuilt on Next.js 16.4.2 (and the first on 14.2.35 too).
Fix: sh: 1: next: not found on Heroku
Three different, unrelated situations produce the exact same Heroku crash — here is how to tell which one is yours before you change anything.


