Next.js use cache Explained: Cache Components, cacheLife & cacheTag (2026 Guide)

Next.js use cache explained: Cache Components, cacheLife and cacheTag banner

Caching in Next.js used to feel like magic you could not control: some fetch calls were cached, some were not, and nobody on the team was quite sure why. The Next.js use cache directive fixes that. Since Next.js 16, you turn on Cache Components, write 'use cache' at the top of a function or component, and you decide exactly what is cached, for how long, and how to refresh it. In this guide you will learn the whole model step by step, with runnable code for Next.js 16.3 (the current Active LTS line).

TL;DR
  • Enable it with cacheComponents: true in next.config.ts. Without it, Next.js 16 is dynamic by default.
  • Add 'use cache' to an async function (data-level) or component (UI-level). Arguments become part of the cache key.
  • Always pair it with cacheLife('hours' | 'days' | ...) and tag it with cacheTag('posts').
  • After a mutation: updateTag() in Server Actions (user sees change now) or revalidateTag(tag, 'max') for background refresh.
  • Anything that reads cookies(), headers() or fresh data goes inside <Suspense>, not inside 'use cache'.

What is the Next.js use cache directive?

'use cache' is a directive, just like 'use client' or 'use server'. You place it as the first line inside an async function, an async Server Component, or at the top of a file. Next.js then stores the return value of that function and reuses it for later calls with the same inputs.

It was introduced as the main caching primitive in Next.js 16.0 (October 2025) and, in 16.3, it also powers client-side caching, Partial Prefetching and “Instant Navigations”. The big idea is simple: nothing is cached unless you say so. That is the opposite of the old App Router behaviour in Next.js 13–14, where fetch was cached implicitly.

Explicit

Only functions marked with 'use cache' are cached. No hidden magic.

‘use cache’

Composable

Cache a single DB query, a component, or an entire page.

data · UI · page

Controllable

Set lifetimes with profiles and invalidate precisely with tags.

cacheLife · cacheTag

Fast by design

Cached output joins the prerendered static shell, served from a CDN.

Partial Prerendering
Note: 'use cache' works only in the App Router and only when Cache Components is enabled. If you are still on the Pages Router, nothing in this article applies to pages/.

The mental model: a static shell with dynamic holes

With Cache Components, Next.js renders each route at build time and sorts every piece of UI into one of three buckets:

  1. Static – pure code, module imports, synchronous reads. Prerendered automatically.
  2. Cached – anything inside 'use cache'. The result is prerendered too and becomes part of the static shell.
  3. Dynamic – uncached fetches, cookies(), headers(), searchParams. These must sit inside <Suspense>. The fallback ships in the shell; the real content streams in at request time.

This approach is called Partial Prerendering (PPR), and it is the default behaviour once Cache Components is on. The diagram below shows how one product page is split.

ONE ROUTE = STATIC SHELL + HOLES Header + Nav (static) Product details ‘use cache’ · cacheLife(‘hours’) Cart cookies() → Suspense Live stock / reviews uncached fetch → streams at request time Static: prerendered at build Cached: in the static shell Dynamic hole: Suspense fallback CDN sends the shell instantly, then the server streams the dashed parts into place. streaming…
Figure 1: With Cache Components, static and cached parts form the shell; dynamic parts stream into Suspense holes.

Step 1: Enable Cache Components

Create a new app (or upgrade an existing one) to the latest Next.js 16 release:

# new project
npx create-next-app@latest my-cache-app
cd my-cache-app

# or upgrade an existing app
npm install next@latest react@latest react-dom@latest

Then switch the feature on in your config file:

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

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig
Tip: In 16.3 you can also add partialPrefetching: true next to it to try Instant Navigations. Start with cacheComponents alone, get your routes clean, then add prefetching.

Once enabled, old route segment options such as export const dynamic = 'force-static', export const revalidate = 60 and fetchCache are no longer used. Remove them and express the same intent with 'use cache' and cacheLife. Next.js will tell you in the terminal if any are left.

Step 2: Data-level caching

The easiest place to start is a data function. Here we cache a list of products coming from a database (or any API):

app/lib/data.ts
import { cacheLife, cacheTag } from 'next/cache'
import { db } from './db'

export async function getProducts() {
  'use cache'
  cacheLife('hours')      // fresh for about an hour
  cacheTag('products')    // label so we can invalidate later

  return db.product.findMany({ orderBy: { createdAt: 'desc' } })
}

export async function getProduct(id: string) {
  'use cache'
  cacheLife('hours')
  cacheTag('products', `product-${id}`)

  return db.product.findUnique({ where: { id } })
}

Notice getProduct(id): the id argument is automatically part of the cache key. getProduct('1') and getProduct('2') get separate entries. Values captured from the surrounding scope are included in the key too.

Rule: arguments and return values must be serializable (strings, numbers, plain objects, arrays, Dates, etc.). You cannot pass class instances or functions as arguments and expect them to be part of the key.

Step 3: UI-level caching with Suspense

You can also cache a whole component. Below is a complete page that mixes all three buckets: static header, a cached post list, and a per-user block that reads cookies.

app/blog/page.tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'

export default function BlogPage() {
  return (
    <>
      {/* 1. Static: prerendered automatically */}
      <header>
        <h1>DevDojo Blog</h1>
      </header>

      {/* 2. Cached: becomes part of the static shell */}
      <PostList />

      {/* 3. Dynamic: fallback in shell, content streams per request */}
      <Suspense fallback={<p>Loading your preferences…</p>}>
        <UserPreferences />
      </Suspense>
    </>
  )
}

type Post = { id: number; title: string }

async function PostList() {
  'use cache'
  cacheLife('days')
  cacheTag('posts')

  const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
  const posts: Post[] = await res.json()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

async function UserPreferences() {
  const theme = (await cookies()).get('theme')?.value ?? 'light'
  return <aside>Your theme: {theme}</aside>
}

Run npm run build and you will see this route marked as partially prerendered. The header and post list are in the HTML immediately; only <UserPreferences /> waits for the request.

Why does reading cookies not make the whole page dynamic? In the old model, one cookies() call opted the entire route into dynamic rendering. Now the <Suspense> boundary contains it, so the rest of the page stays static.

Step 4: Control freshness with cacheLife

cacheLife() tells Next.js how long a cached result stays good. Each profile has three timings:

  • stale – how long the browser can reuse the data without asking the server.
  • revalidate – after this, the next request gets the cached copy and triggers a background refresh (stale-while-revalidate).
  • expire – after this with no traffic, the next request waits for fresh data.

These are the built-in profiles in Next.js 16.3:

ProfileGood forstalerevalidateexpire
defaultUsed when you don’t call cacheLife5 min15 minnever
secondsLive scores, prices30 s1 s1 min
minutesSocial feeds, news5 min1 min1 hour
hoursInventory, weather5 min1 hour1 day
daysBlog posts, articles5 min1 day1 week
weeksPodcasts, newsletters5 min1 week30 days
maxLegal pages, CMS content + webhook5 min30 days1 year

You can also define your own named profiles in the config, or pass an inline object for a one-off case:

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

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    editorial: {
      stale: 600,      // 10 minutes
      revalidate: 3600, // 1 hour
      expire: 86400,    // 1 day
    },
  },
}

export default nextConfig
// anywhere inside a 'use cache' scope
cacheLife('editorial')

// or inline (seconds)
cacheLife({ stale: 60, revalidate: 300, expire: 3600 })
Short-lived caches become dynamic holes. The seconds profile, revalidate: 0, or an expire under 5 minutes is excluded from the prerendered shell. Wrap such components in <Suspense>. Also, if you nest a short-lived cache inside another 'use cache' that has no explicit cacheLife, Next.js throws during prerender, which is one more reason to always set it.

Step 5: cacheTag, updateTag and revalidateTag

Time-based refresh is fine for many pages, but when a user creates or edits something you want to refresh now. That is what tags are for. You tag cached data with cacheTag(), then invalidate by tag after a change.

ON-DEMAND INVALIDATION WITH TAGS User submits a form handled by a Server Action Server Action createPost() CMS calls a Route Handler webhook Webhook route.ts updateTag(‘posts’) revalidateTag(‘posts’,’max’) expire now · read-your-writes serve stale · refresh in background CACHE ENTRIES getPosts() · tag: posts <PostList/> · tag: posts getUsers() · tag: users only “posts” entries refresh
Figure 2: Tags let you refresh exactly the cache entries affected by a change, nothing more.

updateTag: the user should see their change immediately

Use updateTag inside a Server Action when the person who made the change must see it on the next screen (read-your-own-writes):

app/blog/actions.ts
'use server'

import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
import { db } from '@/app/lib/db'

export async function createPost(formData: FormData) {
  const post = await db.post.create({
    data: {
      title: String(formData.get('title')),
      content: String(formData.get('content')),
    },
  })

  updateTag('posts')          // expire every entry tagged 'posts' right now
  redirect(`/blog/${post.id}`)
}
app/blog/new/page.tsx
import { createPost } from '../actions'

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Write something…" required />
      <button type="submit">Publish</button>
    </form>
  )
}

revalidateTag: refresh in the background

Use revalidateTag when a small delay is fine, for example when your CMS sends a webhook. It works in Server Actions and Route Handlers. In Next.js 16 the second argument (a cacheLife profile) is required; 'max' is the recommended value.

app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import type { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-webhook-secret')
  if (secret !== process.env.WEBHOOK_SECRET) {
    return Response.json({ ok: false }, { status: 401 })
  }

  const { tag } = await request.json() // e.g. { "tag": "posts" }
  revalidateTag(tag, 'max')            // stale-while-revalidate

  return Response.json({ ok: true, revalidated: tag })
}

Test it locally with curl:

curl -X POST http://localhost:3000/api/revalidate \
  -H "Content-Type: application/json" \
  -H "x-webhook-secret: $WEBHOOK_SECRET" \
  -d '{"tag":"posts"}'
updateTagrevalidateTagrevalidatePath
WhereServer Actions onlyServer Actions + Route HandlersServer Actions + Route Handlers
BehaviourExpires immediatelyStale-while-revalidateEverything for one path
Use whenUser must see own changeCMS/webhook, slight delay OKYou don’t know the tags
Tip: Prefer tags over paths. revalidatePath('/') can throw away far more cache than you intended.

Step 6: Cookies, headers and per-user data

You cannot call cookies(), headers() or read searchParams directly inside a plain 'use cache' function, because the result would be shared between users. The recommended pattern is: read the runtime value outside, then pass it in as an argument. The argument becomes part of the cache key.

app/profile/page.tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'

export default function ProfilePage() {
  return (
    <Suspense fallback={<p>Loading profile…</p>}>
      <ProfileContent />
    </Suspense>
  )
}

// Not cached: reads the cookie at request time
async function ProfileContent() {
  const userId = (await cookies()).get('userId')?.value ?? 'guest'
  return <CachedProfile userId={userId} />
}

// Cached per userId
async function CachedProfile({ userId }: { userId: string }) {
  'use cache'
  cacheLife('minutes')

  const res = await fetch(`https://jsonplaceholder.typicode.com/users/1?u=${userId}`)
  const user = await res.json()
  return <h2>Hello, {user.name}</h2>
}

Next.js 16 also ships two variants of the directive for special cases:

  • 'use cache: private' – can read cookies/headers directly; the result is cached only in that user’s browser (great for prefetching personalised UI).
  • 'use cache: remote' – stores entries in a shared, durable cache handler instead of per-instance memory. Useful on serverless platforms, where in-memory entries don’t survive between requests.

Need a truly unique value per request, like a request ID or the current time? Call connection() first and wrap the component in Suspense:

import { connection } from 'next/server'

async function RequestId() {
  await connection() // defer to request time
  return <p>Request ID: {crypto.randomUUID()}</p>
}

If you are new to async patterns like these, our guide on mastering JavaScript Promises is a good refresher, since Server Components are just async functions under the hood.

Interactive: which caching tool should I use?

Click a tab to match your data to the right pattern.

Public content: blog posts, product catalogue, docs

Use 'use cache' + cacheLife('days') (or 'max' for CMS content) + cacheTag(). Trigger revalidateTag(tag, 'max') from a webhook when content changes. Result: served from the static shell, refreshed only when needed.

User-generated content: comments, posts, settings

Cache reads with 'use cache' + cacheTag('comments'). In the Server Action that saves the data, call updateTag('comments') so the author sees the change on the very next render.

Personalised UI: dashboard, cart, profile

Read cookies() inside a component wrapped in <Suspense>, then pass the user ID to a cached child (cache key per user). Or use 'use cache: private' for browser-only caching that can be prefetched.

Real-time data: stock prices, live scores, request IDs

Don’t cache at all, or use cacheLife('seconds'). Put the component inside <Suspense> with a good fallback. Use await connection() before Date.now(), Math.random() or crypto.randomUUID().

Common errors and fixes

Error / symptomWhy it happensFix
Uncached data was accessed outside of <Suspense> (shown as a “blocking-route” insight in 16.3)A component fetches fresh data or reads cookies/headers with no Suspense boundary above it.Wrap it in <Suspense fallback={...}>, or cache it with 'use cache'.
Error using cookies() / headers() inside 'use cache'Runtime APIs can’t run in a shared cache scope.Read the value outside and pass it as an argument, or use 'use cache: private'.
“blocking-prerender-current-time” / “-random” / “-crypto”Date.now(), Math.random() or crypto.randomUUID() during prerender.await connection() + Suspense, or move it inside 'use cache' if one shared value is fine.
Error about export const revalidate / dynamicRoute segment configs are replaced by Cache Components.Delete them; use cacheLife() in the cached scope instead.
Data never updates after savingNo tag on the cached function, or tag names don’t match.Add cacheTag('x') and call updateTag('x') with the exact same string.
Error during prerender about nested short-lived cacheA cacheLife('seconds') component is used inside a 'use cache' scope with no explicit cacheLife.Add an explicit cacheLife() to the outer scope.
Cache seems to reset on every request (serverless)Default runtime cache is in-memory per instance.Use 'use cache: remote' with a configured cache handler for high hit-rate data.
“Only plain objects can be passed…” style serialization errorsPassing a class instance or function as a cached argument / return value.Pass IDs or plain objects; return plain JSON-like data.

Best practices

  • Always call cacheLife() in every 'use cache' scope, right at the top. It makes behaviour obvious to the next developer.
  • Tag generously, invalidate narrowly. Use a broad tag ('posts') and a specific one (`post-${id}`) on the same entry.
  • Push dynamic work deep down the tree. The lower your cookies() or uncached fetch sits, the more of the page stays in the static shell.
  • Keep cached return values small. Return only the fields the UI needs, not whole ORM objects.
  • Don’t cache per-user data in a shared scope unless the user ID is an argument. Never leak one user’s data to another.
  • Use updateTag for forms, revalidateTag(tag, 'max') for webhooks. Avoid the old single-argument revalidateTag(tag); it is deprecated in Next.js 16.
  • Write good Suspense fallbacks. Skeletons that match the final layout prevent layout shift and improve Core Web Vitals.
  • Check the dev overlay. In 16.3, Instant Insights and the Navigation Inspector show exactly which parts are blocking.

Migrating from the old caching model

Old (Next.js 14/15)New (Cache Components)
fetch(url, { next: { revalidate: 3600 } })Wrap the fetch in a 'use cache' function with cacheLife('hours')
fetch(url, { next: { tags: ['posts'] } })cacheTag('posts') inside the cached function
unstable_cache(fn, keys, opts)'use cache' directive inside fn
export const revalidate = 60cacheLife({ revalidate: 60 })
export const dynamic = 'force-dynamic'Default behaviour; just use Suspense

The official Next.js caching documentation and the cacheLife API reference have more detail and a step-by-step migration guide.

FAQ

Is “use cache” stable in Next.js 16?

Yes. Cache Components and the 'use cache' directive shipped as a stable, opt-in feature in Next.js 16.0 and were extended in 16.3. You enable it with cacheComponents: true.

Do I still need to cache fetch() calls?

With Cache Components, fetch is not cached by default. If you want a fetch result reused, put it inside a function or component marked with 'use cache'.

What is the difference between updateTag and revalidateTag?

updateTag expires the cache immediately and works only in Server Actions, so the user sees their own change. revalidateTag(tag, 'max') serves stale data while refreshing in the background and also works in Route Handlers.

Can I use “use cache” in a Client Component?

No. It runs on the server. You can cache a Server Component and render Client Components inside it, or pass cached data down to them as props.

Where is the cache stored?

Prerendered results live in the static shell (on disk or your host’s CDN). Runtime entries live in memory per server instance by default, 'use cache: remote' uses a shared cache handler, and 'use cache: private' lives only in the browser. All caches are scoped to one deployment.

Does this work when self-hosting on a VPS or Docker?

Yes. On a long-running Node.js server the in-memory cache persists between requests, so plain 'use cache' works well. For multiple instances, configure a shared cache handler.

Conclusion

The Next.js use cache directive turns caching from guesswork into a clear, readable decision in your code. Turn on cacheComponents, mark slow and shareable work with 'use cache', give it a lifetime with cacheLife, label it with cacheTag, and put everything request-specific behind <Suspense>. Do that, and your pages load from a static shell instantly while still showing fresh data exactly where it matters.

A good next step: take one slow page in your project, split it into static, cached and dynamic parts like Figure 1, and measure the difference. And if you are also exploring AI tooling, check out our hands-on guide on how to build an MCP server in TypeScript.

New posts every day on DevDojo

Practical tutorials on React, Next.js, Node.js, Python, AI and interview prep, written in simple English.

Bookmark DevDojo and drop your Cache Components questions in the comments below.

Explore more tutorials →
Share