Cache Components and `useCache`: Next.js 16’s New Caching Model
use cacheis the most transformative API in Next.js 16—it elevates caching from a "side effect of data fetching" to a "first-class citizen at the component level."
1. What You'll Learn
- The
use cache()Command and the Concept of Content Cache cacheTag()Tagging cached contentcacheLife()Set the cache expiration timerevalidateTag()On-Demand Cache Expiration- The difference between implicit fetch caching and explicit
use cachecaching
2. A True Story of a Systems Architect
(1) Pain Point: Four identical API calls on the same page
While reviewing the TaskFlow Dashboard, Charlie noticed that the four components on the page—<UserAvatar>, <UserGreeting>, <UserStats>, and <UserNotifications>—were each calling fetch('/api/user'). While the cache hit for the same URL worked fine, the database query function getUserFromDB() was called four times. The fetch cache only applies to HTTP requests; it is completely ineffective against internal server function calls.
| Question | Data |
|---|---|
| Page-level database calls | 4 identical queries |
| Time per query | 200 ms |
| Total Additional Time | 600 ms Wasted |
| Database QPS Waste | 4x |
(2) Solution to use cache
Wrap the function in
use cache()—this caches any internal function calls, including database queries, calculations, and file reads.
// app/dashboard/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getUserData() {
'use cache'
cacheTag('user-data')
cacheLife({ stale: 300, revalidate: 600 })
const user = await db.user.findUnique({ where: { id: 1 } }) // Execute only once
return user
}
export default async function DashboardPage() {
const user = await getUserData() // The first call executes a database query
// Subsequent calls will return the cached result directly.
return (
<div>
<UserAvatar user={user} />
<UserGreeting user={user} />
<UserStats user={user} />
<UserNotifications user={user} />
</div>
)
}
(3) Revenue
| Dimension | Traditional Model | use cache |
|---|---|---|
| Number of database queries | 4 | 1 |
| Additional delay | 600 ms | 0 ms |
| Cache Granularity | URL Level | Function/Component Level |
| Custom logic cache | ❌ Not supported (HTTP fetch only) | ✅ Supports any code |
3. The use cache() Directive and Content Cache
use cache is a function-level directive—add 'use cache' at the top of a function to indicate that the function’s result should be cached. The cached content is called the Content Cache, a brand-new caching layer introduced in Next.js 16 that is independent of the traditional Data Cache (fetch cache).
graph TB
subgraph "Next.js 16 Cache System"
A[Request Memoization<br/>Request-level memory]
B[Data Cache<br/>fetch cache]
C[Content Cache<br/>use cache]
D[Full Route Cache<br/>Full Route Caching]
end
A --> E[Within the same request<br/>The same fetch Remove duplicates]
B --> F[Cross-Request Persistence<br/>force-cache/no-store]
C --> G[Caching the Results of Arbitrary Functions<br/>tag + life Control]
D --> H[Page-level HTML cache]
style C fill:#d4edda
| Cache Level | Scope | Trigger | Lifecycle |
|---|---|---|---|
| Request Memoization | Single Request | Automatic (Same URL) | End of Request |
| Data Cache | Cross-Request | fetch(options) |
Configuration-Dependent |
| Content Cache | Cross-Request | 'use cache' Directive |
cacheLife + cacheTag |
| Full Route Cache | Cross-request | Build-time / Runtime | Revalidate / On-demand |
(1) Basic Syntax
// app/cache-demo/actions.ts
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
export async function getExpensiveData(id: string) {
'use cache'
cacheTag('expensive', `id-${id}`) // Tagging
cacheLife({ stale: 60, revalidate: 300 }) // 60 Return within seconds stale,300 Try again in a few seconds
// All operations within this function are cached.
const result = await db.query(...)
return result
}
(2) cacheLife Configuration
| Parameter | Type | Description | Example |
|---|---|---|---|
stale |
number (seconds) | Returns the result directly while the cache is valid; does not trigger a background refresh | { stale: 60 } |
revalidate |
number (seconds) | Re-execute the function after this time has elapsed | { revalidate: 3600 } |
expire |
number (seconds) | Cache expires absolutely; force re-fetch | { expire: 86400 } |
▶ Example: Basic use cache Usage (Difficulty ⭐)
// app/cache-basic/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getServerTime() {
'use cache'
cacheTag('server-time')
cacheLife({ stale: 10, revalidate: 30 })
// Simulate time-consuming operations
await new Promise(resolve => setTimeout(resolve, 1000))
return { time: new Date().toISOString(), server: process.env.HOSTNAME ?? 'local' }
}
export default async function CacheBasicPage() {
const [t1, t2, t3] = await Promise.all([
getServerTime(),
getServerTime(),
getServerTime(),
])
return (
<div>
<h1>use cache — Basic</h1>
<p>Call 1: {t1.time}</p>
<p>Call 2: {t2.time}</p>
<p>Call 3: {t3.time}</p>
<p><em>All three calls returned the same cached result (no duplicate computation)</em></p>
</div>
)
}
Output:
Call 1: 2026-07-06T10:00:00.000Z
Call 2: 2026-07-06T10:00:00.000Z ← Milliseconds of cache overlap
Call 3: 2026-07-06T10:00:00.000Z
4. Practical Use of cacheTag and cacheLife
cacheTag() Labels cached content, cacheLife() controls the expiration policy. Combined with revalidateTag(), this enables fine-grained cache control.
(1) Tagging and Categorization Strategy
graph TB
A[Application Data] --> B[User Data]
A --> C[Product Data]
A --> D[Order Data]
B --> E[tag: user-profile]
B --> F[tag: user-settings]
C --> G[tag: products]
C --> H[tag: product-{id}]
D --> I[tag: orders]
D --> J[tag: order-{id}]
style A fill:#cce5ff
| Policy | Tag Examples | Invalid Operations | Scope |
|---|---|---|---|
| Fine-grained | product-42 |
revalidateTag('product-42') |
Single product only |
| Medium Grain | products |
revalidateTag('products') |
All Products |
| Coarse-grained | catalog |
revalidateTag('catalog') |
Entire directory |
(2) Dynamic Tags
// app/products/[id]/page.tsx
import { unstable_cacheTag as cacheTag } from 'next/cache'
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return <ProductView product={product} />
}
async function getProduct(id: string) {
'use cache'
cacheTag('products', `product-${id}`) // Dynamic tags based on parameters
return fetch(`https://api.example.com/products/${id}`).then(r => r.json())
}
▶ Example: Tiered Caching Strategy (Difficulty: ⭐⭐)
// app/cache-strategy/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getUserProfile(userId: number) {
'use cache'
cacheTag('users', `user-${userId}`)
cacheLife({ stale: 120, revalidate: 600 }) // 2 minutes stale,10 minutes revalidate
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then(r => r.json())
}
async function getUserPosts(userId: number) {
'use cache'
cacheTag('posts', `user-posts-${userId}`)
cacheLife({ stale: 60, revalidate: 300 })
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}
export default async function CacheStrategyPage() {
const [profile, posts] = await Promise.all([
getUserProfile(1),
getUserPosts(1),
])
return (
<div>
<h1>{profile.name}</h1>
<p>Email: {profile.email}</p>
<h2>Posts ({posts.length})</h2>
<ul>{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
</div>
)
}
▶ Example: Filtering by tag in Server Action doesn't work (Difficulty: ⭐⭐)
// app/cache-invalidation/page.tsx
import { unstable_cacheTag as cacheTag, revalidateTag } from 'next/cache'
async function getTaskList() {
'use cache'
cacheTag('tasks')
return fetch('https://jsonplaceholder.typicode.com/todos?_limit=5').then(r => r.json())
}
export default async function CacheInvalidationPage() {
const tasks = await getTaskList()
return (
<div>
<h1>Task List</h1>
<ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
<form action={async () => {
'use server'
revalidateTag('tasks') // Click the button to refresh the task list cache immediately
}}>
<button type="submit">Refresh Tasks</button>
</form>
</div>
)
}
5. Implicit fetch Caching vs. Explicit use cache Caching
| Dimension | Implicit fetch cache (Data Cache) | Explicit use cache (Content Cache) |
|---|---|---|
| Trigger Method | fetch(url) Automatic |
Function Body 'use cache' Instruction |
| Cached Content | HTTP Response | Result returned by any function |
| Use Cases | API calls, HTTP requests | Database queries, complex calculations, file reading |
| Tag System | next: { tags: [...] } |
cacheTag() Function |
| Time Control | next: { revalidate: N } |
cacheLife() Function |
| Failure Mode | revalidateTag() / revalidatePath() |
revalidateTag() (same label) |
(1) When should you use fetch cache, and when should you use use cache?
| Scenario | Recommended Caching Method | Reason |
|---|---|---|
| Call External APIs | Data Cache (fetch) |
Native Support for HTTP Semantics |
| Database Call | Content Cache (use cache) |
Database call is not an HTTP fetch |
| Complex computations (50 ms+) | Content Cache | Any function can be cached |
| Read Files/Configuration | Content Cache | File operations cannot be performed via fetch |
| Mixed HTTP + DB Processing | Content Cache | Caching Entire Blocks of Logic |
▶ Example: Combining Two Caches (Difficulty: ⭐⭐⭐)
// app/cache-combo/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
// 1. fetch cache: Call External API
async function getExternalPosts() {
return fetch('https://jsonplaceholder.typicode.com/posts', {
next: { tags: ['external-posts'], revalidate: 300 }
}).then(r => r.json())
}
// 2. use cache:Processed data
async function getProcessedPosts() {
'use cache'
cacheTag('processed-posts')
cacheLife({ stale: 60, revalidate: 600 })
const raw = await getExternalPosts() // Dependency fetch cache
return (raw as any[]).map((p: any) => ({
id: p.id,
title: p.title.toUpperCase(),
summary: p.body.slice(0, 100),
}))
}
export default async function CacheComboPage() {
const posts = await getProcessedPosts()
return (
<div>
<h1>Processed Posts ({posts.length})</h1>
<ul>{posts.map((p: any) => (
<li key={p.id}><strong>{p.title}</strong><p>{p.summary}</p></li>
))}</ul>
</div>
)
}
6. Complete Example: Caching Architecture for a User Management System
// app/cache-system/page.tsx — A Complete Caching Architecture
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
// ======== Data Layer(use cache Explicit Caching) ========
type User = { id: number; name: string; email: string }
type Post = { id: number; title: string; body: string }
async function getUsers(): Promise<User[]> {
'use cache'
cacheTag('users')
cacheLife({ stale: 120, revalidate: 600 })
return fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json())
}
async function getUserPosts(userId: number): Promise<Post[]> {
'use cache'
cacheTag('posts', `user-posts-${userId}`)
cacheLife({ stale: 60, revalidate: 300 })
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}
async function getStats(users: User[]) {
'use cache'
cacheTag('stats')
cacheLife({ stale: 300, revalidate: 1800 })
return {
totalUsers: users.length,
avgNameLength: users.reduce((s, u) => s + u.name.length, 0) / users.length,
}
}
// ======== Page Components ========
export default async function CacheSystemPage() {
const users = await getUsers()
const stats = await getStats(users)
return (
<div style={{ maxWidth: 900, margin: '0 auto', padding: 24 }}>
<h1>User Management</h1>
<StatsCard stats={stats} />
<div style={{ display: 'grid', gap: 16, marginTop: 24 }}>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
</div>
)
}
// ======== Child component(Independent Cache) ========
async function UserCard({ user }: { user: User }) {
const posts = await getUserPosts(user.id)
return (
<div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
<h2>{user.name}</h2>
<p style={{ color: '#666' }}>{user.email}</p>
<details>
<summary>Posts ({posts.length})</summary>
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
</details>
</div>
)
}
function StatsCard({ stats }: { stats: { totalUsers: number; avgNameLength: number } }) {
return (
<div style={{ background: '#f0f4ff', borderRadius: 8, padding: 16, display: 'flex', gap: 32 }}>
<div><strong>Total Users</strong><p style={{ fontSize: 24 }}>{stats.totalUsers}</p></div>
<div><strong>Avg Name Length</strong><p style={{ fontSize: 24 }}>{stats.avgNameLength.toFixed(1)}</p></div>
</div>
)
}
// ======== Administrative Tasks ========
// app/cache-system/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshUserData() {
revalidateTag('users') // Refresh the user list
}
export async function refreshPosts(userId: number) {
revalidateTag(`user-posts-${userId}`) // Refresh a specific user's posts
}
export async function refreshAll() {
revalidateTag('users')
revalidateTag('posts')
revalidateTag('stats')
}
// app/cache-system/admin-button.tsx
'use client'
export function AdminControls() {
return (
<div style={{ display: 'flex', gap: 8, margin: '16px 0' }}>
<form action={async () => {
const { refreshUserData } = await import('./actions')
await refreshUserData()
}}>
<button type="submit">Refresh Users</button>
</form>
<form action={async () => {
const { refreshAll } = await import('./actions')
await refreshAll()
}}>
<button type="submit">Refresh All</button>
</form>
</div>
)
}
▶ Example: Verifying a Cache Hit (Difficulty: ⭐⭐)
// app/cache-verify/page.tsx — Check whether the cache hit
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
let callCount = 0
async function getUniqueId() {
'use cache'
cacheTag('unique-id')
cacheLife({ revalidate: 30 })
callCount++
return { id: crypto.randomUUID(), calls: callCount }
}
export default async function CacheVerifyPage() {
const [a, b, c] = await Promise.all([
getUniqueId(),
getUniqueId(),
getUniqueId(),
])
return (
<div>
<h1>Cache Verification</h1>
<p>Result A: {a.id} (call #{a.calls})</p>
<p>Result B: {b.id} (call #{b.calls})</p>
<p>Result C: {c.id} (call #{c.calls})</p>
<p><strong>Same ID + calls = 1 → cache hit</strong></p>
</div>
)
}
Output:
Result A: 550e8400-e29b-41d4-a716-446655440000 (call #1)
Result B: 550e8400-e29b-41d4-a716-446655440000 (call #1) ← Cache Hit
Result C: 550e8400-e29b-41d4-a716-446655440000 (call #1) ← Cache Hit
❓ FAQ
use cache and useMemo?useMemo is a client-side hook that caches computations only within the browser and is scoped to a single render. use cache is a server-side directive that persists the cache across requests, supports tags and expiration times, and allows Server Actions to invalidate it on demand. The two have completely different use cases.fetch and use cache?revalidateTag('products') will clear both the entry labeled next: { tags: ['products'] } from the fetch data cache and the entry labeled cacheTag('products') from the content cache. This is the key mechanism for achieving unified cache invalidation.use cache be used in a client component?'use cache' directive is only valid in server components or server functions. In client components, you should use useMemo or client-side caching solutions such as React Query.unstable_ prefix in unstable_cacheLife and unstable_cacheTag mean?unstable_ prefix is typically removed after 1–2 major releases (expected in Next.js 17 or 18 stable).📖 Summary
- The
'use cache'directive makes the results of any server-side function cacheable, not limited to HTTP fetches cacheTag()Tagging cached content; supports dynamic tags (based on parameters)cacheLife()Control the cache expiration policy (stale + revalidate dual mode)revalidateTag()Clear all corresponding tags from the fetch cache and the content cache at once- Implicit
fetchcaching (Data Cache) is suitable for HTTP requests, while explicituse cache(Content Cache) is suitable for any function. - Caching granularity from coarse to fine: page-level → data-level → function-level; use
cacheto implement function-level caching - Best practice: Use
use cachefor database queries, complex calculations, and data processing functions; usefetchcaching for HTTP APIs.
📝 Exercises
-
Basic Problem (⭐): Create a
app/cache-demo/page.tsxand use a'use cache'function to wrap a simulated time-consuming operation (await new Promise(resolve => setTimeout(resolve, 2000))). Call it three times on the page and verify that the second call starts with zero delay (cache hit). -
Advanced Exercise (⭐⭐): Build a page that includes a list of users and a list of user articles. Use
cacheLife({ revalidate: 300 })for the user list andcacheLife({ revalidate: 60 })for the article list. Add two Server Action buttons at the bottom of the page to refresh the user tab and the article tab, respectively. Verify that the tabs function independently. -
Challenge (⭐⭐⭐): Implement a "Data Analytics Dashboard" that retrieves data from three different sources (HTTP API + simulated database + calculation functions) and uses
use cachefor centralized caching. Each data source has a differentcacheLifestrategy. Add a "Force Refresh All" button that callsrevalidateTagto refresh all dashboards simultaneously. Build a tool to track cache hit rates.



