use cache and Cache Components
The idea in one sentence
Section titled “The idea in one sentence”Cache Components flips the model: instead of remembering per-fetch cache options, you mark a function, file, or component with the 'use cache' directive to say “cache this,” then shape its lifetime and invalidation with cacheLife and cacheTag.
The use cache directive
Section titled “The use cache directive”Add the string 'use cache' at the top of a function (or file) and everything it returns is cached — a fetch, a database query, or a whole rendered component. It replaces scattering cache options across individual calls.
import { cacheLife, cacheTag } from 'next/cache'
async function getData() { 'use cache' cacheLife('hours') cacheTag('data') const res = await fetch('https://api.example.com/data') return res.json()}From unstable_cache to use cache
Section titled “From unstable_cache to use cache”Code written against Next.js 14 (or an AI model trained on it) still uses unstable_cache — wrap the function, pass a key-parts array, pass an options object. The bundled docs now say plainly: “This API has been replaced by use cache in Next.js 16.” It still works (it has not been removed), but the mapping is worth knowing cold, because it comes up in every review of older code:
unstable_cache | 'use cache' equivalent |
|---|---|
unstable_cache(fn, keyParts, options) wraps the function from outside | 'use cache' goes inside the function itself, as its first line |
keyParts: ['posts'] — a manual cache-key suffix | Not needed — 'use cache' derives the key from the function’s own arguments and closure automatically |
options.tags: ['posts'] | cacheTag('posts'), called inside the function |
options.revalidate: 3600 (seconds) | cacheLife({ revalidate: 3600 }), or a named profile like cacheLife('hours') |
Works with cacheComponents off | Requires cacheComponents: true |
| Docs status in Next.js 16 | Deprecated, still importable — not removed |
import { unstable_cache } from 'next/cache'import { db } from '@/lib/db'
// Old model: keyParts is a manual cache-key suffix; tags/revalidate live in// the options object.export const getPosts = unstable_cache( async () => db.post.findMany(), ['posts'], { tags: ['posts'], revalidate: 3600 },)import { cacheLife, cacheTag } from 'next/cache'import { db } from '@/lib/db'
// New model: no manual key — 'use cache' derives it from the function's// arguments and closure. Tag and lifetime are set inline, in scope.export async function getPosts() { 'use cache' cacheTag('posts') cacheLife({ revalidate: 3600 }) return db.post.findMany()}export const db = { post: { findMany: async () => [] as { id: string; title: string }[], },}cacheLife and cacheTag
Section titled “cacheLife and cacheTag”cacheLife sets how long the entry stays fresh — a named profile like 'hours' or an explicit object. Next.js ships six named profiles besides default, each balancing the same three timing properties (stale, revalidate, expire):
| Profile | Use case | stale | revalidate | expire |
|---|---|---|---|---|
default | Standard content | 5 min | 15 min | never |
seconds | Real-time data (stock prices, live scores) | 30 s | 1 s | 1 min |
minutes | Frequently updated (social feeds, news) | 5 min | 1 min | 1 hour |
hours | Multiple daily updates (product inventory, weather) | 5 min | 1 hour | 1 day |
days | Daily updates (blog posts, articles) | 5 min | 1 day | 1 week |
weeks | Weekly updates (podcasts, newsletters) | 5 min | 1 week | 30 days |
max | Rarely changes (legal pages, archived content) | 5 min | 30 days | 1 year |
You can also define your own named profile in next.config.ts, alongside or instead of using the built-in names — see Custom cache profiles below for a full example built and checked in the probe.
cacheTag attaches a tag so you can invalidate the entry on demand, and a single call can attach more than one: cacheTag('products', 'product-42') tags the same entry both broadly (every product) and specifically (this one product), so a mutation can invalidate at whichever granularity makes sense. revalidateTag(tag, 'max') marks a tag stale and serves the old entry while refreshing (in Next.js 16 the second argument is required — the one-argument form is deprecated and a TypeScript error), and inside a Server Action updateTag(tag) expires it immediately so the user reads their own write.
import { cacheLife, cacheTag } from 'next/cache'
async function getProduct(id: string) { 'use cache' cacheTag('products', `product-${id}`) cacheLife({ stale: 60, revalidate: 300 }) return db.product.findUnique({ where: { id } })}Turning it on: cacheComponents and Partial Prerendering
Section titled “Turning it on: cacheComponents and Partial Prerendering”'use cache' requires the cacheComponents flag. In Next.js 16 that same flag is also how you opt into Partial Prerendering — the older experimental ppr flag and experimental_ppr export were removed. With it on, Next.js prerenders a static shell and streams the dynamic, uncached holes in at request time.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true,}
export default nextConfigCustom cache profiles
Section titled “Custom cache profiles”Extend the same config file with your own named cacheLife profiles, alongside the flag:
// next.config.ts — extends the minimal config above with a custom profileimport type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true, cacheLife: { biweekly: { stale: 60 * 60 * 24 * 14, // 14 days revalidate: 60 * 60 * 24, // 1 day expire: 60 * 60 * 24 * 14, // 14 days }, },}
export default nextConfig// app/catalog/page.tsx — only type-checks against the config above, not the harness's shared probe configimport { cacheLife } from 'next/cache'
async function getData() { 'use cache' cacheLife('biweekly') return { now: Date.now() }}
export default async function CatalogPage() { const data = await getData() return <p>{data.now}</p>}cacheLife’s type overloads for a custom profile name are generated from whichever next.config.ts is live at the probe root during next dev/next build/next typegen — a lesson fence named next.config.ts does not itself change that for the shared type-check harness, which is why this pair was verified with a real, isolated next build (below) rather than left for automatic collection.
Built and checked in the probe, the build output confirms the custom profile’s exact numbers made it through:
Route (app) Revalidate Expire┌ ○ /├ ○ /_not-found└ ○ /catalog 1d 2wPrivate and remote caches
Section titled “Private and remote caches”Two variants extend the model, both shipped with Cache Components in Next.js 16. 'use cache: private' may read cookies(), headers(), and searchParams directly; its result is never written to a shared server cache — it is kept per browser, in the client’s memory for the stale window, so it is never shared across visitors. 'use cache: remote' stores the entry in a shared, durable cache handler (Redis, KV, or whatever the platform provides via cacheHandlers) so every server instance and every user hits the same entry, at the cost of a network round trip.
import { cacheLife } from 'next/cache'import { cookies } from 'next/headers'
async function getRecommendations() { 'use cache: private' cacheLife({ stale: 60 }) const sessionId = (await cookies()).get('session-id')?.value return db.recommendations.forSession(sessionId)}flowchart TD A["cacheComponents: true"] --> B["'use cache' function or component"] A --> C["Partial Prerendering"] B --> D["Cached static shell"] C --> D D --> E["Dynamic uncached holes stream in"]
Mental Model
Section titled “Mental Model”'use cache' moves the caching decision from the call site (an options object on each fetch) to the definition site (a directive at the top of the function). Once you are inside a 'use cache' scope, cacheLife and cacheTag are just local configuration for that one cached unit — they never leak out, and nothing about how the function is called from outside needs to change. That is the whole shift from the old model: caching becomes a property of the function, not of each call.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
'use cache' on a function | No manual cache key, tags/lifetime live next to the logic they describe | Requires cacheComponents: true — an all-or-nothing flag for the route segment config it replaces |
Built-in cacheLife profiles | A name communicates intent ('days' reads as “updates daily”) | The exact numbers are fixed unless you override them — read the table if precision matters |
Custom cacheLife profile in next.config.ts | Exact control, reusable by name across the app | One more thing to keep in sync — the name only means what the config currently says it means |
Multiple cacheTag values on one entry | Invalidate broadly ('products') or narrowly ('product-42') from different mutations | Every extra tag is one more thing that must be revalidated correctly, or a change looks invalidated when it is not |
Common Misconceptions
Section titled “Common Misconceptions”- “
unstable_cachewas removed in Next.js 16.” It was not — it still compiles and runs, but the docs mark it replaced by'use cache'and recommend migrating. - “
'use cache'needs a manual cache key likeunstable_cache’skeyParts.” It does not — the directive derives the key from the function’s own arguments and closure automatically. - “Adding a second
cacheTagreplaces the first one.” It does not —cacheTag('products', 'product-42')attaches both tags to the same entry; either one alone can invalidate it later. - “
'use cache: private'is just a shorter-lived version of the regular cache.” It is a different storage location entirely — never written to a shared server cache, kept per browser instead, specifically so it is safe to readcookies()/headers()inside it.
Reading AI-generated code
Section titled “Reading AI-generated code”When a generated snippet uses unstable_cache or 'use cache', check for these before trusting it:
- Is
unstable_cacheused with notagsin its options object? Without a tag, nothing can ever target that entry withrevalidateTag— it can only expire on itsrevalidatetimer or a full redeploy, which silently defeats on-demand invalidation. - Does a
'use cache'function callcacheLifeat all? Omitting it does not error — it silently falls back to thedefaultprofile (15-minute revalidate), which is easy to mistake for “no caching.” - Is
cacheTagcalled with a single tag when the entry logically needs two (a collection-level tag and an item-level tag)?cacheTagaccepts multiple values in one call — no need for two separate calls or to pick just one granularity. - Does the code assume
'use cache'works withoutcacheComponents: trueinnext.config.ts? It does not — the directive requires the flag, and old route-segment-config exports (dynamic,revalidate,fetchCache) fail the build once it is on.
🐛 Spot the bug
import { unstable_cache } from 'next/cache'import { db } from '@/lib/db'
export const getPosts = unstable_cache(async () => {return db.post.findMany()})Show the answer
No tags were passed in the (omitted) options object, and no keyParts array was passed either. The bundled docs describe tags as what revalidateTag uses to target an entry — without one, this cached result can only ever go stale on its own timer (there is no revalidate here either, so it is cached indefinitely) or when the app redeploys. A Server Action that later calls revalidateTag('posts') after creating a post will do nothing to this entry, because it was never tagged 'posts' — a bug that produces no error, no warning, just stale data forever. The fix is unstable_cache(async () => db.post.findMany(), ['posts'], { tags: ['posts'] }), or better, migrate it to 'use cache' with an explicit cacheTag('posts').