- Next.js 16.2 Caching Explained: unstable_cache vs use cache
Next.js 16.2 Caching Explained: unstable_cache vs use cache
Clear guidance on unstable_cache, Cache Components, and 'use cache' in Next.js 16—practical advice for Payload CMS…

⚡ Next.js Implementation Guides
In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
I was integrating Payload CMS into an existing Next.js app recently. The app was already running on Next.js 16.2+, which should mean I have access to all the modern caching tooling. I open the docs, see unstable_cache marked as legacy, read that use cache is the new recommended directive, and then realize that Cache Components are not actually enabled in this project.
So what am I supposed to use?
If you have hit that same wall, this article is for you. The confusion is not a reading comprehension problem. It comes from three genuinely separate things being discussed as though they are one caching story. Once you understand what each piece actually is, the decision becomes straightforward.
Why Next.js 16.2 Caching Feels Confusing
The short answer is that developers are conflating three distinct concepts:
unstable_cache, which is a data-layer caching API- Cache Components, which is an application-level rendering and caching model
use cache, which is a new directive that only makes sense once Cache Components are enabled
Documentation and community discussion often skip over the distinction between the second and third point. People see unstable_cache labelled legacy, read that use cache is the replacement, and assume they can swap one for the other. But use cache does not live in the same layer as unstable_cache. They solve overlapping problems but are not drop-in replacements for each other without a broader architectural change.
Let us go through each piece properly.
What unstable_cache Actually Is
unstable_cache is a server-side function that caches the result of an async computation. You wrap a database query, a CMS fetch, or any expensive server-side call, and Next.js caches the return value across requests.
// File: app/lib/get-posts.ts
import { unstable_cache } from "next/cache";
import { getPayload } from "payload";
import config from "@payload-config";
export const getCachedPosts = unstable_cache(
async () => {
const payload = await getPayload({ config });
const posts = await payload.find({
collection: "posts",
limit: 20,
});
return posts.docs;
},
["posts-list"],
{
revalidate: 3600,
tags: ["posts"],
}
);
The function takes three arguments: the async function to cache, a key array that identifies this cache entry, and an options object where you set revalidate for time-based expiry or tags for on-demand revalidation via revalidateTag.
What this does is straightforward. The first call executes the function and stores the result. Subsequent calls within the revalidation window return the cached result without hitting the database or CMS again. You can also call revalidateTag("posts") from a webhook or route handler to invalidate the cache on demand.
The "unstable" prefix has been there since it was introduced. It does not mean broken — it means the API signature was subject to change. In current docs it is labelled legacy, but it still works exactly as it always has and it is present in a large number of production Next.js projects.
What Cache Components Actually Are
Cache Components are not a function or a directive. They are an application-level feature that changes how the App Router handles rendering and caching by default.
When Cache Components are enabled, the rendering model shifts. Request-time data is no longer cached unless you explicitly opt in. Components and functions can declare their own caching behavior using use cache. The framework treats caching as something you opt into deliberately, rather than something that happens automatically at the fetch or route level.
Enabling Cache Components requires opting in via your Next.js config:
// File: next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
cacheComponents: true,
},
};
export default nextConfig;
This is an experimental flag and it changes the behavior of the entire application. It is not a feature you enable in isolation for one route or one component. Enabling it on an existing app means you need to review how caching works across the whole project.
That context matters. In a greenfield project, you would likely enable this from the start. In an existing production app with established caching patterns, enabling it mid-project is a decision that needs careful planning.
What use cache Actually Is
use cache is a directive, similar in concept to "use client" or "use server". You add it at the top of a file, a component, or a function, and it tells Next.js to cache that piece of work.
// File: app/components/PostList.tsx
"use cache";
import { getPayload } from "payload";
import config from "@payload-config";
export async function PostList() {
const payload = await getPayload({ config });
const posts = await payload.find({
collection: "posts",
limit: 20,
});
return (
<ul>
{posts.docs.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
You can also use it at the function level rather than the file level, which gives you more granular control:
// File: app/lib/get-posts.ts
import { unstable_cacheTag as cacheTag, unstable_cacheLife as cacheLife } from "next/cache";
async function getCachedPosts() {
"use cache";
cacheTag("posts");
cacheLife("hours");
const payload = await getPayload({ config });
const posts = await payload.find({
collection: "posts",
limit: 20,
});
return posts.docs;
}
Notice cacheTag and cacheLife. These are the companion APIs for use cache. cacheTag applies a revalidation tag the same way the tags option does in unstable_cache. cacheLife lets you reference named cache profiles instead of raw seconds, which reads more cleanly for shared codebases.
use cache is the intended path forward for Next.js caching. It is more declarative, composable, and consistent with the directive model the framework already uses. But it is designed to work within a Cache Components setup. Without that flag enabled, its behavior is either unavailable or unreliable depending on the Next.js version.
Why These Are Not the Same Thing
This is where the confusion collapses into something clear.
unstable_cache is an API. You call it, you wrap a function, you get back a cached version of that function. It works at the data layer and does not depend on how the rest of your app is configured.
Cache Components is the rendering and caching model. It is a project-level architectural choice. Enabling it changes what caching means across the entire application.
use cache is the directive that lives inside the Cache Components model. It declares caching intent at the file, component, or function level. It is the developer-facing API for that model.
The relationship is: Cache Components is the foundation, use cache is the tool you use on top of it. unstable_cache is a separate, older tool that operates outside that model.
Switching from unstable_cache to use cache is not just swapping a function call. It is adopting a different caching architecture. In a new project, that is the obvious right path. In an existing project, it is a deliberate migration decision with real scope.
Real-World Case: Payload CMS in an Existing Next.js 16 App
Here is the scenario I was actually working with.
An existing Next.js application, already running on 16.1+. An established codebase with existing caching patterns. Cache Components not enabled. I was brought in to integrate Payload CMS as the content layer — an isolated concern, scoped carefully to avoid disrupting what was already working.
In that context, enabling Cache Components was not on the table. The host development team had not planned for it, had not reviewed the caching implications app-wide, and had no immediate reason to take on that migration as part of a CMS integration project.
The right tool for that situation was unstable_cache.
// File: app/lib/payload-cache.ts
import { unstable_cache } from "next/cache";
import { getPayload } from "payload";
import config from "@payload-config";
export const getCachedPage = unstable_cache(
async (slug: string) => {
const payload = await getPayload({ config });
const result = await payload.find({
collection: "pages",
where: { slug: { equals: slug } },
limit: 1,
});
return result.docs[0] ?? null;
},
["page-by-slug"],
{
revalidate: 3600,
tags: ["pages"],
}
);
This is clean, scoped, and does not touch anything else in the application. The Payload integration gets its own caching layer. The rest of the app continues working exactly as before. On-demand revalidation via revalidateTag("pages") hooks into a Payload webhook cleanly.
Using unstable_cache here was not ignorance of the newer API. It was a deliberate compatibility choice. That distinction matters.
When unstable_cache Is Still the Right Choice
There are real situations where unstable_cache is the appropriate tool, even in a project running on Next.js 16.
If you are working in an existing production application where Cache Components are not enabled, using use cache without enabling that flag is not a supported pattern. The "legacy" label in the docs is a forward-looking signal, not a deprecation notice for running code.
If you are scoping an isolated piece of work — a CMS integration, a new data layer, an added feature — and the rest of the codebase has no immediate plans to adopt the newer caching model, the pragmatic choice is the one that introduces the least disruption.
If you are building something that will eventually migrate to use cache, unstable_cache does not create a structural mess. The pattern is similar enough that migration is straightforward once the team is ready.
The framework continues to support it. The mental model carries forward. Use it confidently where it fits.
When to Migrate to use cache
The right moment to migrate is when the application as a whole is ready to adopt Cache Components.
That means the team has decided to enable the experimental flag, has audited how existing caching behavior changes under the new model, and has the capacity to update caching declarations across the affected parts of the app.
At that point, use cache is the direction to move toward. It is more composable, reads more clearly in component-heavy codebases, and aligns with where the framework is heading. cacheTag and cacheLife are cleaner than managing raw key arrays and numeric revalidation values. The directive model fits naturally alongside "use client" and "use server" in the App Router pattern.
If you are starting a new project today with full control over the config from the beginning, enable Cache Components and use use cache from the start. That is the intended path.
The Practical Decision Tree
When you hit a caching decision in Next.js 16, work through this in order.
Does this data need to be fresh on every request? Do not cache it. Fetch it directly inside your component or server action.
Are you caching HTTP responses via fetch? Use the fetch cache options — next: { revalidate: 3600 } or next: { tags: ["tag"] }. That layer works independently of everything else.
Are you caching a database query, a CMS call, or another server-side computation? Check whether Cache Components are enabled in the project.
If Cache Components are enabled, use use cache with cacheTag and cacheLife. That is the intended modern path.
If Cache Components are not enabled, use unstable_cache. It is the right tool for that context, regardless of what the docs call it.
Conclusion
The best caching choice is not always the newest API. It is the one that fits the current project architecture without creating unnecessary migration risk.
unstable_cache, Cache Components, and use cache are three things that belong to overlapping but distinct layers. Seeing one marked legacy does not mean your existing approach is wrong. It means the framework has a preferred direction for new projects and for teams ready to adopt it.
Know which layer you are working in. Know whether Cache Components are enabled. Make the call that fits the project, not the one that looks most modern in isolation.
Let me know in the comments if you have questions, and subscribe for more practical development guides.
Thanks, Matija