'use cache'
Cache a file, component, or async function's output keyed on its inputs.
'use cache'- Added in Next.js v15.0.0
Return value
Nothing directly; the covered function returns its own value, served from cache when the inputs match a stored entry.
Errors
Reading cookies(), headers() or searchParams inside the scope — including from a helper it calls — throws next-request-in-use-cache. Awaiting a Promise of request data created outside the boundary instead times out the build after 50 seconds. Non-serializable arguments such as class instances error when the cache key is computed.
Changelog
- PHP v15.0.0"use cache" is introduced as an experimental feature.
- PHP v16.0.0"use cache" is enabled with the Cache Components feature.
'use cache' marks a route, component, or async function as cacheable. At the top of a file it covers every export; inside a function it covers that function's return value. Enable it with cacheComponents: true in next.config.
The cache key is the thing to understand
An entry is keyed on the build ID, a hash of the function's identity, and its serialized arguments — including variables captured from an outer scope, which are bound as arguments automatically. Two calls with the same inputs share one entry; a new deploy invalidates everything, because the build ID moved.
What trips people up
- Every covered export must be
async. At file level that includes framework exports such asgenerateMetadata. cookies(),headers()andsearchParamsare unavailable, and the ban follows the call stack into helpers. On a dynamic route this can passnext buildand fail undernext start. Read them outside and pass values in — or use 'use cache: private'.- Passing a Promise of request data in instead hangs the build for 50 seconds before erroring, which is the same mistake wearing a different error message.
- Slots pass through untouched: a cached layout does not cache its
children. - Storage is in-memory by default, so on serverless it often misses between requests. That is what 'use cache: remote' is for.
Example
'use cache'
import { cacheLife, cacheTag } from 'next/cache'
export async function getMonthlyTotals(accountId: string) {
cacheLife('hours')
cacheTag(`totals-${accountId}`)
return db.orders.aggregate({
where: { accountId },
_sum: { amount: true },
})
}
export async function getTopProducts() {
cacheLife('days')
cacheTag('products')
return db.products.findMany({ orderBy: { sales: 'desc' }, take: 10 })
}When to use it
- Prerender a product page's data into the static shell while keeping the price dynamic.
- Cache one expensive database read shared by several components on a route.
- Cache an entire route segment by putting the directive at the top of page.tsx and layout.tsx.
Frequently asked questions
Why can't I use cookies() or headers() inside 'use cache'?
Because the entry is shared across users and keyed only on its inputs, so request-specific values would leak between them. The restriction follows the call stack, so a helper that reads them fails the same way with the next-request-in-use-cache error. Read the values outside the cached scope and pass them in, or use 'use cache: private'.
Does 'use cache' persist across deploys?
No. The cache key includes the build ID (or deploymentId when configured), so a new build produces new keys and the previous build's entries are unreachable. For entries that must survive a deploy, use unstable_cache for non-fetch functions or the fetch cache.
Why does my build hang for 50 seconds with 'use cache'?
You are awaiting a Promise inside the cached scope that resolves to request-time data created outside it — passed as a prop, captured in a closure, or pulled from a shared Map. It can never resolve during prerendering, so the fill times out. Resolve the value outside the boundary and pass the plain value in.
More examples
Passing request data in as arguments
Lives at app/page.tsx. This is the fix for next-request-in-use-cache. Moving the cookie read out turns a per-user, uncacheable component into a handful of entries — one per language — that all users share. Note that cookies() is async and awaited.
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'
export default async function Page() {
// Read request APIs OUTSIDE the cached scope
const language = (await cookies()).get('language')?.value ?? 'en'
const content = await getContent(language)
return <div>{content.welcome}</div>
}
async function getContent(language: string) {
'use cache'
cacheLife('hours')
// Cached per language, shared by every user with that preference
return cms.getHomeContent(language)
}Interleaving: children are not cached
Lives at app/page.tsx. JSX passed into a slot is rendered by the parent and handed through, so it never becomes part of the cache entry. That is how a cached shell can wrap fully dynamic content — as long as the cached function does not read the slot's contents.
import type { ReactNode } from 'react'
export default async function Page() {
const uncachedData = await getData()
return (
<CacheComponent header={<h1>Home</h1>}>
<DynamicComponent data={uncachedData} />
</CacheComponent>
)
}
async function CacheComponent({
header,
children,
}: {
header: ReactNode
children: ReactNode
}) {
'use cache'
const res = await fetch('https://api.example.com/cached-data')
const cachedData = await res.json()
return (
<div>
{header}
<PrerenderedComponent data={cachedData} />
{children}
</div>
)
}Nested caches and inherited lifetimes
Lives at lib/orders.ts. Each accountId gets its own entry at both levels, and a summary hit skips the inner query entirely. The explicit cacheLife on the outer scope matters: without it, a shorter inner lifetime would silently shorten the outer one.
import { cacheLife } from 'next/cache'
export async function getOrderSummary(accountId: string) {
'use cache'
cacheLife('hours')
const orders = await getOrders(accountId)
const totals = await getOrderTotals(accountId)
return { orders, totals }
}
export async function getOrders(accountId: string) {
'use cache'
cacheLife('hours')
return db.orders.findMany({ where: { accountId } })
}
// Deliberately uncached: callers outside the summary read fresh totals
export async function getOrderTotals(accountId: string) {
return db.orders.aggregate({ where: { accountId }, _sum: { amount: true } })
}
Discussion