| | |

Next.js 08 ▲ Caching — ‘use cache’ directives & Cache Components model

⏱️ Estimated read: 14 min | |

Next.js 16 replaced the implicit caching behavior of earlier App Router versions with an explicit, opt-in model called Cache Components. Data fetching is dynamic by default. Nothing is cached unless you say so, and you say so with the 'use cache' directive. The directive marks a function, a component, or an entire file as cacheable. Next.js then treats its output as part of the static shell that can be prerendered at build time and served from a CDN, while the rest of the route streams in dynamically behind <Suspense> boundaries.

This chapter covers the Cache Components model and the 'use cache' directive: enabling the feature in next.config.ts, placing the directive at file, component, and function scope, controlling cache lifetime with cacheLife, invalidating cache entries with cacheTag and updateTag, and the constraints that prevent a cached function from reading request-time APIs like cookies() or headers(). It also explains what changed from the previous fetch cache options and unstable_cache, and why the in-memory default has different persistence characteristics than the old Data Cache.

Key point: 'use cache' is opt-in. Data is dynamic unless you wrap it. The directive creates a cache entry keyed by the function’s serializable arguments, and the entry lives in memory by default, not across deployments or serverless instances.


Why Cache Components replaced the old model

The implicit caching problem. In earlier App Router versions, fetch requests were cached by default, and route segments could be made static with export const dynamic = 'force-static'. This created a mental model where caching was the default and opting out required explicit configuration. The result was predictable for some apps and surprising for others, especially when a fetch was cached when the developer expected fresh data, or when a route became dynamic unexpectedly because of a cookies() call buried deep in a component .

The granularity problem. The old model operated at the route level. A route was either static or dynamic, and fetch caching was controlled per-request. Cache Components moves the decision to the function and component level. A page can contain a cached header, an uncached user-specific widget, and a cached product list, all in the same route. The static shell is built from the cached parts, and the dynamic parts stream in .

The replacement of unstable_cache. The unstable_cache function was the previous way to cache a database query or computation. Cache Components replaces it with the 'use cache' directive and the cacheLife and cacheTag functions. The cache key is derived automatically from the function’s arguments, so the manual key-parts array is no longer needed . Existing unstable_cache calls still work during migration, but new code should use 'use cache'.


a. Enabling Cache Components

Cache Components is enabled with the cacheComponents flag in next.config.ts. The partialPrefetching flag should be set explicitly; leaving it unset logs a warning because the first release of Cache Components did not include Partial Prefetching .

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
}

export default nextConfig

Cache Components requires the Node.js runtime. The runtime = 'edge' route segment configuration is not supported and must be removed. The flag also implies Partial Prerendering (PPR) as the default behavior, so the separate experimental.ppr and experimental_ppr configuration options are no longer necessary .

b. Placing the directive

The 'use cache' directive can be placed at three scopes: the top of a file, the top of a component body, or the top of a function body.

// File level: all exports in the file are cacheable
'use cache'

export async function getProducts() {
  return db.product.findMany()
}

// Component level: the component's output is cached
export async function ProductList() {
  'use cache'
  const products = await db.product.findMany()
  return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
}

// Function level: the function's return value is cached
export async function getStats() {
  'use cache'
  const res = await fetch('https://api.example.com/stats')
  return res.json()
}

When placed at the top of a layout or page file, the directive prerenders that route segment. To prerender an entire route, both the layout and the page need the directive, because each is treated as a separate entry point .

c. Controlling lifetime with cacheLife

By default, a 'use cache' scope uses the default profile: stale: 5 minutes on the client, revalidate: 15 minutes on the server, and expire: never by time. The cacheLife function overrides this with a built-in profile or an explicit configuration.

import { cacheLife } from 'next/cache'

export async function getPosts() {
  'use cache'
  cacheLife('hours') // revalidate: 1 hour, expire: 1 day
  return db.post.findMany()
}

The built-in profiles are 'seconds', 'minutes', 'hours', 'days', 'weeks', and 'max'. Custom profiles can be defined in next.config.ts with explicit stale, revalidate, and expire values. The Next.js documentation recommends setting an explicit cacheLife in every use cache scope so the cache behavior is clear at the call site, rather than depending on the default profile .

d. Invalidating with cacheTag and updateTag

Time-based revalidation is one mechanism; on-demand invalidation is the other. The cacheTag function attaches a tag to a cached scope, and updateTag (or revalidateTag) invalidates every entry with that tag.

// Tag the cached data
import { cacheTag } from 'next/cache'

export async function getProducts() {
  'use cache'
  cacheTag('products')
  return db.product.findMany()
}
// Invalidate from a Server Action
'use server'
import { updateTag } from 'next/cache'

export async function updateProduct(id: string, data: ProductData) {
  await db.product.update({ where: { id }, data })
  updateTag('products') // next request to getProducts revalidates
}

The two mechanisms are often paired. A blog post that changes only when the author edits it can use cacheLife('max') with a cacheTag, then invalidate on demand when the post is saved. A list of recent posts that updates throughout the day can use cacheLife('hours') and refresh on its own .

e. Constraints: request-time APIs and isolation

A cached function executes in an isolated environment. It cannot read request-time APIs like cookies(), headers(), or searchParams directly. These values must be read outside the cached scope and passed in as arguments. This constraint exists because the cache key is derived from the function’s arguments; a request-time value read inside the function would make the cache entry depend on data that is not part of the key .

// Wrong: cached function reads cookies directly
async function CachedGreeting() {
  'use cache'
  const user = (await cookies()).get('user') // error
  return <p>Hello, {user}</p>
}

// Correct: read outside, pass as argument
async function CachedGreeting({ user }: { user: string }) {
  'use cache'
  return <p>Hello, {user}</p>
}

export default async function Page() {
  const user = (await cookies()).get('user')?.value ?? 'guest'
  return <CachedGreeting user={user} />
}

React.cache also operates in an isolated scope inside a use cache boundary. Values stored via React.cache outside the cached function are not visible inside it. This isolation ensures cached functions have predictable, self-contained behavior .


Complete Example Session

// ============================================
// PART 1: ENABLE CACHE COMPONENTS
// ============================================
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
}

export default nextConfig

// Cache Components requires the Node.js runtime.
// Remove any runtime = 'edge' export.
// ============================================
// PART 2: CACHE A FUNCTION
// ============================================
// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache'

export async function getProducts() {
  'use cache'
  cacheLife('hours')
  cacheTag('products')
  return db.product.findMany()
}

// The cache key is derived from the function's arguments.
// No arguments means a single shared cache entry.
// ============================================
// PART 3: CACHE A COMPONENT
// ============================================
export async function ProductList() {
  'use cache'
  const products = await getProducts()
  return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
}

// The component's rendered output is cached.
// The cache entry is reused when the serialized props match.
// ============================================
// PART 4: CACHE AN ENTIRE ROUTE
// ============================================
// app/products/layout.tsx
'use cache'
export default function Layout({ children }: { children: ReactNode }) {
  return <div>{children}</div>
}

// app/products/page.tsx
'use cache'
export default function Page() {
  return <ProductList />
}

// Both layout and page need the directive to prerender the route.
// ============================================
// PART 5: CUSTOMIZE LIFETIME
// ============================================
import { cacheLife } from 'next/cache'

export async function getStats() {
  'use cache'
  cacheLife('minutes') // revalidate: 1 minute
  return fetch('https://api.example.com/stats').then((r) => r.json())
}

// Built-in profiles: seconds, minutes, hours, days, weeks, max
// Custom profiles can be defined in next.config.ts.
// ============================================
// PART 6: INVALIDATE ON DEMAND
// ============================================
// app/actions.ts
'use server'
import { updateTag } from 'next/cache'

export async function updateProduct(id: string, data: ProductData) {
  await db.product.update({ where: { id }, data })
  updateTag('products')
}

// The next request to getProducts() sees the fresh data.
// updateTag integrates with both client and server caches.
// ============================================
// PART 7: THE REQUEST-TIME CONSTRAINT
// ============================================
// Wrong
async function CachedWidget() {
  'use cache'
  const user = (await cookies()).get('user') // error
  return <div>{user}</div>
}

// Correct
async function CachedWidget({ user }: { user: string }) {
  'use cache'
  return <div>{user}</div>
}

// Read request-time APIs outside the cache scope
// and pass the values as arguments.
// ============================================
// PART 8: MIX CACHED AND DYNAMIC CONTENT
// ============================================
// app/page.tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'

async function CachedPosts() {
  'use cache'
  return <Posts />
}

async function Greeting() {
  const user = (await cookies()).get('user')?.value
  return <p>Welcome back, {user}</p>
}

export default function Page() {
  return (
    <main>
      <CachedPosts /> {/* in the static shell */}
      <Suspense fallback={<p>Loading greeting...</p>}>
        <Greeting /> {/* streams per request */}
      </Suspense>
    </main>
  )
}
// ============================================
// PART 9: RUNTIME CACHE BEHAVIOR
// ============================================
// Serverless:
//   Cache entries typically don't persist across requests.
//   Build-time caching works normally.
//
// Self-hosted:
//   Cache entries persist across requests.
//   Control size with cacheMaxMemorySize.
//
// For durable storage, use use cache: remote
// with a cache handler (Redis, KV database).
// ============================================
// PART 10: MIGRATION FROM fetch CACHE OPTIONS
// ============================================
// Before:
// fetch(url, { cache: 'force-cache', next: { revalidate: 3600 } })
//
// After:
async function getData() {
  'use cache'
  cacheLife('hours')
  return fetch(url).then((r) => r.json())
}

// Fetches inside use cache are cached automatically.
// Fetches outside are uncached.

The ten parts covered enabling Cache Components, caching a function, caching a component, caching a route, customizing lifetime, on-demand invalidation, the request-time constraint, mixing cached and dynamic content, runtime cache behavior, and migrating from fetch cache options.


Quick Reference

Directives and functions

APIPurpose
'use cache'Mark a scope as cacheable
cacheLife(profile)Set cache lifetime
cacheTag(tag)Attach an invalidation tag
updateTag(tag)Invalidate by tag from a Server Action
revalidateTag(tag)Alternative invalidation function

Built-in cacheLife profiles

ProfileRevalidateExpire
'seconds'1 second60 seconds
'minutes'1 minute1 hour
'hours'1 hour1 day
'days'1 day1 week
'weeks'1 week30 days
'max'30 days365 days

Scope and behavior

ScopePlacementEffect
FileTop of fileAll exports cacheable
ComponentTop of component bodyRendered output cached
FunctionTop of function bodyReturn value cached
RouteTop of both layout and pageRoute segment prerendered

Constraints

ConstraintDetail
Request-time APIscookies(), headers(), searchParams not allowed inside
React.cache isolationValues from outside not visible inside
Serializable argumentsCache key derived from serializable props/args
RuntimeNode.js only, no Edge
Side effectsNot allowed in cached functions

Best Practices

✅ Do This:

// Set an explicit cacheLife in every cached scope
'use cache'
cacheLife('hours')                                       // ✅
// Tag cached data for on-demand invalidation
cacheTag('products')                                     // ✅
// Read cookies outside the cache scope and pass as arguments
const user = (await cookies()).get('user')?.value        // ✅
<CachedComponent user={user} />
// Cache shared subqueries, not per-user pages
// The page stays dynamic; the shared query is cached      // ✅

❌ Don’t Do This:

// Read request-time APIs inside a cached scope
'use cache'
const user = (await cookies()).get('user')               // ❌ (error)
// Assume use cache persists across deployments
// It is in-memory by default                             // ❌
// Cache a component that already calls a cached query
// Double-caching, no benefit                             // ❌
// Use use cache: private for shared data
// Private is browser-only, not server-cached             // ❌

Common Pitfalls

PitfallWhy It HappensFix
Build fails with “uncached data outside Suspense”Async work without use cache or <Suspense>Wrap in <Suspense> or add 'use cache'
Cached function reads cookies() and failsRequest-time API inside cache scopeRead outside, pass as argument
Cache entry not invalidated after mutationNo updateTag or revalidateTag callTag the cached data and invalidate on mutation
Cache persists across deployments unexpectedlyuse cache is in-memory by defaultUse use cache: remote for durable storage
React.cache value missing inside cached functionReact.cache scope is isolatedPass data as function arguments instead
runtime = 'edge' causes errorsCache Components requires Node.jsRemove the runtime export

Real-World Examples

1. Enable Cache Components

{ cacheComponents: true, partialPrefetching: true }

2. Cache a database query

'use cache'
cacheLife('hours')
cacheTag('products')
return db.product.findMany()

3. Cache a component

export async function ProductList() {
  'use cache'
  return <ul>{/* ... */}</ul>
}

4. Cache a route

// Both layout.tsx and page.tsx start with 'use cache'
'use cache'

5. Invalidate with updateTag

'use server'
updateTag('products')

6. Custom cache profile

// next.config.ts
cacheLife: { 'blog-post': { stale: 300, revalidate: 3600, expire: 86400 } }

7. Pass user from cookies

const user = (await cookies()).get('user')?.value
<CachedGreeting user={user} />

8. Mix static and dynamic

<CachedPosts />
<Suspense fallback={<p>Loading...</p>}>
  <Greeting />
</Suspense>

9. Use cache: remote for durable storage

'use cache: remote'

10. Migrate from fetch cache

// Before: fetch(url, { cache: 'force-cache' })
// After: 'use cache' inside the function

Visual

The Cache Components model

┌─────────────────────────────────────────────────────────────┐
│  CACHE COMPONENTS MODEL                                      │
│                                                              │
│  Static Shell (prerendered at build)                         │
│    ├── 'use cache' component output                          │
│    ├── Synchronous content                                   │
│    └── <Suspense> fallbacks                                  │
│                                                              │
│  Dynamic Holes (streamed at request time)                    │
│    ├── Async work without 'use cache'                        │
│    ├── cookies() / headers() reads                           │
│    └── Per-user data                                         │
│                                                              │
│  Both coexist in a single route.                             │
│  The shell is served instantly.                              │
│  The holes stream in when ready.                             │
└─────────────────────────────────────────────────────────────┘

The use cache directive scope

┌─────────────────────────────────────────────────────────────┐
│  THREE SCOPES                                                │
│                                                              │
│  File level:                                                 │
│    'use cache'                                               │
│    export async function a() { ... }                         │
│    export async function b() { ... }                         │
│    (all exports cached)                                      │
│                                                              │
│  Component level:                                            │
│    export async function Comp() {                            │
│      'use cache'                                             │
│      return <div>{...}</div>                                 │
│    }                                                         │
│    (rendered output cached)                                  │
│                                                              │
│  Function level:                                             │
│    export async function getData() {                         │
│      'use cache'                                             │
│      return await fetch(...)                                 │
│    }                                                         │
│    (return value cached)                                     │
└─────────────────────────────────────────────────────────────┘

Cache key derivation

┌─────────────────────────────────────────────────────────────┐
│  CACHE KEY                                                  │
│                                                              │
│  Inputs:                                                     │
│    Build ID (per build)                                     │
│    Function ID (secure, unique)                             │
│    Serializable arguments / props                           │
│                                                              │
│  Same arguments ──► same cache entry                        │
│  Different arguments ──► different cache entry              │
│                                                              │
│  Non-serializable values (JSX children) are opaque.         │
│  They are passed through but not part of the key.           │
└─────────────────────────────────────────────────────────────┘

Runtime vs build-time caching

┌─────────────────────────────────────────────────────────────┐
│  PERSISTENCE                                                 │
│                                                              │
│  Build-time:                                                 │
│    Cache entry created during prerender                     │
│    Included in the static shell                             │
│    Served from CDN                                          │
│                                                              │
│  Runtime (in-memory default):                                │
│    Serverless: may not persist across requests              │
│    Self-hosted: persists, size-limited by cacheMaxMemorySize│
│                                                              │
│  Durable (use cache: remote):                                │
│    External cache handler (Redis, KV)                       │
│    Survives instance teardown                               │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Directive'use cache'
ConfigurationcacheComponents: true in next.config.ts
Default behaviorData is dynamic unless cached
Lifetime controlcacheLife(profile)
InvalidationcacheTag + updateTag / revalidateTag
ScopeFile, component, function, route
Request-time APIsNot allowed inside cache scope
Default profilestale 5m, revalidate 15m, never expires
Runtime persistenceIn-memory, serverless may not persist
Durable storageuse cache: remote with cache handler

Key takeaways:

  • Cache Components is opt-in. Data is dynamic by default. Nothing is cached unless wrapped in 'use cache' or placed inside a <Suspense> boundary. This reverses the earlier App Router default and makes caching explicit .
  • 'use cache' works at file, component, and function scope. At the top of a layout and page, it prerenders the route segment. At the top of a component, it caches the rendered output. At the top of a function, it caches the return value .
  • The cache key is derived from serializable arguments. No manual key array is needed, unlike unstable_cache. Non-serializable values like JSX children are passed through but not part of the key .
  • Request-time APIs must be read outside the cache scope. cookies(), headers(), and searchParams cannot be read inside a 'use cache' function. Read them outside and pass the values as arguments .
  • cacheLife sets the lifetime; cacheTag enables on-demand invalidation. The two are often paired: a long-lived cache with a tag that is invalidated when the underlying data changes .
  • In-memory caching does not survive serverless instance teardown. The default 'use cache' stores entries in memory. For durable storage across instances and deployments, 'use cache: remote' with a cache handler is required .

Remember: Cache Components replaces the implicit caching of earlier App Router versions with an explicit directive. The 'use cache' directive marks what should be cached; <Suspense> marks what should stream. The two work together: the cached parts form the static shell that is served instantly, and the uncached parts stream in behind fallbacks. Set an explicit cacheLife in every cached scope, tag the data you need to invalidate, and keep request-time APIs outside the cache boundary. The old fetch cache options and unstable_cache still work during migration, but new code should use 'use cache' and its companion functions.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!