The unstable_cache wrapper is replaced by use cache in Next.js 16. The migration is mostly mechanical: drop the key-parts array, turn the wrapper into a function marked with the directive, and map the options to cacheLife and cacheTag. Moving off the old wrapper also lets the function join the static shell, which unstable_cache could not do.
Before and after
The old wrapper builds a cache key from a key-parts array and configures revalidation through an options object. Here is a typical function that fetches one post and caches it for an hour:
// app/lib/posts.ts
import { unstable_cache } from 'next/cache'
export const getPost = unstable_cache(
async (id: string) => {
const res = await fetch(`https://api.example.com/posts/${id}`)
return res.json()
},
['post'],
{ tags: ['posts'], revalidate: 3600 }
)The new version uses the directive instead of a wrapper, and the key-parts array disappears because arguments derive the key.
// app/lib/posts.ts
import { cacheLife, cacheTag } from 'next/cache'
export async function getPost(id: string) {
'use cache'
cacheLife('hours')
cacheTag('posts')
const res = await fetch(`https://api.example.com/posts/${id}`)
return res.json()
}The id argument now keys the entry on its own, cacheTag replaces the tags option, and cacheLife replaces the revalidate seconds. The behavior is the same: the post is cached, tagged, and refreshed on the same schedule as before.
What changes
Four things move, and three of them are one-to-one replacements.
- The key-parts array goes away, because arguments derive the key.
- The tags option maps to a cacheTag call inside the function.
- The revalidate option maps to a cacheLife profile or inline object.
- The wrapped export becomes a plain exported function marked with the directive.
When revalidate does not match a profile
The old wrapper takes any number of seconds, while cacheLife takes a named profile. When the two do not line up, pick the closest preset or define a custom profile in next.config.ts so the name still means what your team expects.
An inline object is the third option, and it keeps a one-off timing at the call site:
// app/lib/rates.ts
import { cacheLife } from 'next/cache'
export async function getRates() {
'use cache'
cacheLife({ revalidate: 2700 })
const res = await fetch('https://api.example.com/rates')
return res.json()
}A 45 minute revalidate has no preset, so the object states it directly. Any timing you leave out falls back to the default profile.
The persistence difference
The old wrapper persists its entries across deployments and serverless instances. use cache does not, because its key includes the build ID and it defaults to in-memory storage.
If you relied on cross-deploy persistence, keep the old wrapper or the fetch data cache for that specific case. The remote variant is not a substitute here: it survives an instance being destroyed, but its key still includes the build, so a new deploy recomputes the value.
Migration steps
Work through one wrapper at a time so you can revalidate each change.
- Enable cacheComponents in next.config.ts if it is not already on.
- Rewrite each wrapper into a function with the use cache directive.
- Replace tags with cacheTag and revalidate with cacheLife.
- Remove the key-parts array and rely on arguments.
- Run the build and confirm the route still prerenders.
Cross-deploy persistence is rare, so most projects can migrate fully and remove the wrapper import.
For the directive's placements, see the use cache directive. For the full three-way comparison, see how the three caches differ.
Rune AI
Key Insights
- Turn the wrapped function into a use cache function.
- Arguments replace the key-parts array.
- The tags option maps to cacheTag.
- The revalidate option maps to cacheLife.
- use cache does not persist across deploys.
Frequently Asked Questions
Do I still need the key-parts array?
Does use cache persist across deployments?
Conclusion
Migrating unstable_cache to use cache is mostly mechanical. Drop the key-parts array, convert the wrapper into a directive-marked function, and map the tags and revalidate options to cacheTag and cacheLife.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.