| name | nextjs-use-cache |
| description | Expert guidance for writing, reviewing, and fixing Next.js caching code using the "use cache" directive, cacheLife, cacheTag, and related invalidation APIs. Trigger this skill whenever the user asks about Next.js caching, data freshness, revalidation, cache invalidation, unstable_cache migration, or performance issues related to fetching in App Router. Also trigger when the user pastes Next.js code that fetches data and asks for a review, optimization, or bug fix — even if they don't say "cache" explicitly. Use this skill proactively when generating new Next.js data-fetching code; never let an agent write uncached server fetches without considering whether use cache applies.
|
Next.js "use cache" Skill
Overview
"use cache" is a React directive (stable in Next.js 15, expanded in 16) that
caches the output of any async function, React component, or an entire
route. It replaces unstable_cache (deprecated in Next.js 16) and manual
fetch() cache options.
Before writing or reviewing any caching code, internalize the decision tree below.
1. Decision Tree — Should I Add "use cache"?
Is this an async Server Component or async function called only on the server?
├── No → Do NOT add "use cache". It does not apply to client components.
└── Yes → Does it fetch external data, run a DB query, or do slow computation?
├── No → Probably fine without it; skip unless output is expensive.
└── Yes → How often does the data change?
├── Per-request (e.g. cart, user session) → No cache; pass data as props
├── Every few seconds → cacheLife('seconds')
├── Every few minutes → cacheLife('minutes')
├── Every hour or multiple daily → cacheLife('hours')
├── Daily → cacheLife('days')
├── Weekly → cacheLife('weeks')
└── Rarely / static → cacheLife('max') or omit (default=15min)
2. Quick-Start Setup
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
blog: { stale: 3600, revalidate: 900, expire: 86400 },
product: { stale: 300, revalidate: 60, expire: 3600 },
},
};
export default nextConfig;
3. The Three Directive Variants
| Directive | Storage | When to use |
|---|
"use cache" | In-memory LRU (per server) | General purpose — use this by default |
"use cache: remote" | External store (Redis/KV) | Multi-instance deploys, persistent shared cache |
"use cache: private" | Browser memory only | Must call cookies()/headers() inside scope |
Default to "use cache". Only reach for variants when you have a specific reason.
4. Built-in cacheLife Profiles
| Profile | stale | revalidate | expire | Best For |
|---|
seconds | 30s | 1s | 1m | Live scores, tickers |
minutes | 5m | 1m | 1h | News feeds, social |
hours | 5m | 1h | 24h | Inventory, weather |
days | 5m | 24h | 1wk | Blog posts, articles |
weeks | 5m | 1wk | 30d | Newsletters, podcasts |
max | 5m | 30d | ∞ | Legal pages, archived content |
| (none) | 5m | 15m | ∞ | Default when cacheLife omitted |
⚠️ Caches with expire < 5 min (e.g. seconds) are excluded from build-time
prerenders and become dynamic holes. Always wrap them in <Suspense>.
5. Invalidation APIs
| API | Use From | Effect |
|---|
cacheTag('tag') | Inside "use cache" scope | Labels entry for later invalidation |
updateTag('tag') | Server Action (preferred) | Marks stale; refetch happens lazily on visit |
revalidateTag('tag','max') | Server Action / Route Handler | Same stale-while-revalidate via profile |
revalidatePath('/path') | Server Action / Route Handler | Invalidates all cached entries for a route |
Always prefer updateTag over bare revalidateTag. Bare revalidateTag
(without a profile) is deprecated and causes a blocking cache miss on next request.
6. Good Practices ✅
6.1 Place "use cache" at the top of the function body, not the file, unless you want every export cached
import { cacheLife, cacheTag } from "next/cache";
export async function getProducts(category: string) {
"use cache";
cacheLife("hours");
cacheTag("products", `products:${category}`);
return db.products.findMany({ where: { category } });
}
6.2 Always call cacheLife immediately after "use cache"
Call it as the first statement so it's obvious and never accidentally skipped.
export async function getBlogPosts() {
"use cache";
cacheLife("days");
cacheTag("blog-posts");
return db.posts.findMany();
}
6.3 Read runtime APIs outside the cached scope, pass values as arguments
cookies(), headers(), and searchParams cannot be called inside "use cache".
import { cookies } from 'next/headers'
export default async function Page() {
const token = cookies().get('auth-token')?.value
const user = await getCachedUser(token)
return <Profile user={user} />
}
async function getCachedUser(token: string | undefined) {
'use cache'
cacheLife('minutes')
cacheTag(`user:${token}`)
if (!token) return null
return db.users.findFirst({ where: { token } })
}
6.4 Use granular tags — tag both the collection AND individual items
export async function getProduct(id: string) {
"use cache";
cacheLife("hours");
cacheTag("products", `product:${id}`);
return db.products.findUnique({ where: { id } });
}
6.5 Mix cached and dynamic content with <Suspense>
import { Suspense } from "react";
async function CachedSidebar() {
"use cache";
cacheLife("days");
return <Sidebar items={await getNavItems()} />;
}
async function LiveFeed() {
return <Feed items={await getLiveItems()} />;
}
export default function Page() {
return (
<div>
<CachedSidebar />
<Suspense fallback={<Spinner />}>
<LiveFeed />
</Suspense>
</div>
);
}
6.6 Use updateTag after mutations in Server Actions
"use server";
import { updateTag } from "next/cache";
export async function publishPost(id: string) {
await db.posts.update({ where: { id }, data: { published: true } });
updateTag("blog-posts");
updateTag(`post:${id}`);
}
6.7 Cache an entire route by annotating BOTH layout and page
'use cache'
import { cacheLife } from 'next/cache'
export default async function BlogLayout({ children }: { children: React.ReactNode }) {
cacheLife('days')
return <div>{children}</div>
}
'use cache'
import { cacheLife, cacheTag } from 'next/cache'
export default async function BlogPage() {
cacheLife('days')
cacheTag('blog-posts')
return <PostList posts={await db.posts.findMany()} />
}
6.8 Pass non-serializable values as pass-through (don't introspect them)
children and Server Actions are non-serializable but can be forwarded safely.
async function CachedShell({ children }: { children: React.ReactNode }) {
"use cache";
cacheLife("hours");
return (
<div>
<CachedNav /> {/* cached data */}
{children} {/* passed through, not cached */}
</div>
);
}
7. Bad Practices ❌
7.1 ❌ Calling cookies() / headers() inside "use cache"
export async function getUser() {
"use cache";
const cookieStore = cookies();
return db.users.findFirst({
where: { token: cookieStore.get("token")?.value },
});
}
export async function getUser(token: string | undefined) {
"use cache";
cacheLife("minutes");
return db.users.findFirst({ where: { token } });
}
7.2 ❌ Omitting cacheLife on frequently-changing data
export async function getInventory(productId: string) {
"use cache";
return db.inventory.findUnique({ where: { productId } });
}
export async function getInventory(productId: string) {
"use cache";
cacheLife("minutes");
cacheTag(`inventory:${productId}`);
return db.inventory.findUnique({ where: { productId } });
}
7.3 ❌ Using a single broad tag for everything
export async function getPost(id: string) {
"use cache";
cacheTag("everything");
return db.posts.findUnique({ where: { id } });
}
export async function getPost(id: string) {
"use cache";
cacheTag("posts", `post:${id}`);
return db.posts.findUnique({ where: { id } });
}
7.4 ❌ Using "use cache" on per-request, user-specific data without tagging
export async function getCart() {
"use cache";
const token = await getToken();
return db.carts.findFirst({ where: { token } });
}
export async function getCart(userId: string) {
"use cache";
cacheLife("seconds");
cacheTag(`cart:${userId}`);
return db.carts.findFirst({ where: { userId } });
}
export async function getCart(userId: string) {
return db.carts.findFirst({ where: { userId } });
}
7.5 ❌ Adding "use cache" to a Client Component
"use client";
export function UserCard({ id }: { id: string }) {
"use cache";
return <div>{id}</div>;
}
async function UserCardServer({ id }: { id: string }) {
"use cache";
cacheLife("minutes");
const user = await db.users.findUnique({ where: { id } });
return <UserCard user={user} />;
}
7.6 ❌ Using bare revalidateTag without a profile (deprecated)
revalidateTag("products");
updateTag("products");
revalidateTag("products", "max");
7.7 ❌ Caching a component that accesses React.cache data from a parent
const store = cache(() => ({ user: null as User | null }));
function Parent() {
store().user = currentUser;
return <Child />;
}
async function Child() {
"use cache";
const { user } = store();
return <div>{user?.name}</div>;
}
async function Child({ userId }: { userId: string }) {
"use cache";
cacheLife("minutes");
const user = await db.users.findUnique({ where: { id: userId } });
return <div>{user?.name}</div>;
}
7.8 ❌ Caching only the page but not the layout (partial prerender gap)
export default async function BlogLayout({ children }) {
const nav = await getNavItems()
return <div>{children}</div>
}
'use cache'
export default async function BlogPage() { ... }
'use cache'
import { cacheLife } from 'next/cache'
export default async function BlogLayout({ children }) {
cacheLife('days')
const nav = await getNavItems()
return <div>{children}</div>
}
7.9 ❌ Using unstable_cache in Next.js 16
import { unstable_cache } from "next/cache";
const getCachedUser = unstable_cache(
async (id: string) => db.users.findUnique({ where: { id } }),
["user"],
);
export async function getCachedUser(id: string) {
"use cache";
cacheLife("minutes");
cacheTag(`user:${id}`);
return db.users.findUnique({ where: { id } });
}
8. Code Review Checklist
When reviewing Next.js data-fetching code, verify each point:
9. Directive Placement Reference
"use cache";
import { cacheLife, cacheTag } from "next/cache";
export async function getPosts() {
cacheLife("days");
cacheTag("posts");
return db.posts.findMany();
}
export async function getAuthors() {
cacheLife("weeks");
cacheTag("authors");
return db.authors.findMany();
}
export async function getPosts() {
"use cache";
cacheLife("days");
cacheTag("posts");
return db.posts.findMany();
}
export async function getUser(id: string) {
return db.users.findUnique({ where: { id } });
}
export async function ProductCard({ id }: { id: string }) {
"use cache";
cacheLife("hours");
cacheTag("products", `product:${id}`);
const product = await db.products.findUnique({ where: { id } });
return <div>{product?.name}</div>;
}
10. Common Error Messages & Fixes
| Error | Cause | Fix |
|---|
cookies() not allowed inside use cache | Called cookies() / headers() inside cached scope | Move call outside; pass value as argument |
| Build hangs for 50 seconds | Awaiting uncached data created outside "use cache" boundary | Move the data source inside the scope or pass result as argument |
| Cache never hits (serverless) | In-memory cache doesn't persist across cold starts | Switch to "use cache: remote" with a persistent cache handler |
| Stale data after mutation | Using bare revalidateTag without profile, or forgot to call updateTag | Use updateTag('tag') after every mutation |
| Two users see same cached user data | User-specific data cached without user-scoped tag | Add cacheTag(\user:${userId}`)` or remove caching |