Skip to main content
Next.js learning series · Blog 8

Next.js 16 Caching and Revalidation Explained

Understand what can be reused, opt into Cache Components deliberately, choose lifetimes and tags, and refresh stale data after mutations.

A central cache serving reusable browser content while an API and database feed a controlled refresh loop

In the previous tutorial, you learned how Next.js Server Actions update data. After a successful database write, the next problem is freshness: will users see the new value, or will an older cached result remain visible?

This guide connects those writes to the read patterns from our Next.js data-fetching tutorial. You will learn the current Next.js 16 cache model, explicit fetch behavior, opt-in Cache Components, "use cache", lifetimes, tags, path invalidation, stale-while-revalidate, database-query caching, private-data safety, and practical debugging.

Caching at a Glance

A cache stores reusable work. A cache hit returns an acceptable stored result. A cache miss must reach the underlying API, database, or computation and may store the new result for later reuse. This is a conceptual model—not a claim that every Next.js request passes through one universal cache.

Diagram 1: Fresh data and a later cached resultA fresh request reaches the database or API, stores a new result, and returns it. A later request can reuse the cached result while it remains valid.
Two request lanes showing immediate cache reuse and a cache miss reaching servers and a database before storing a result
A cache hit and miss do different work. A hit can reuse an acceptable entry; a miss reaches the source and can populate the cache for a later request.

Why Does Next.js Cache Anything?

Appropriate caching can avoid repeated computations, reduce duplicate database or API work, improve suitable response paths, and help an application scale. None of those benefits are automatic. Cache lookup, serialization, storage, invalidation, and network distance all have costs.

The central trade-off is freshness. A reusable result can become stale after its source changes. Revalidation controls when Next.js should refresh or discard that result. Good cache design begins with two questions: “Is this safe to reuse?” and “How fresh must it be?”

The Current Next.js 16 Caching Model

Next.js 16 introduces Cache Components as an opt-in model. When enabled with cacheComponents: true, data work is excluded from prerenders unless you explicitly cache it. The route can combine a prerendered shell, cached sections, and request-time dynamic sections.

Project status

This repository is a PHP publishing site. It has no package.json, next.config.*, installed Next.js version, or Cache Components setting. The examples below document the Next.js 16 feature without modifying project configuration or claiming it is enabled here.

Opt-in configuration in a real Next.js 16 app
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig
Diagram 2: Cache Components mental modelA route can contain prerenderable structure, explicitly cached work, and uncached request-time work. Cached entries may be reused until time-based or on-demand revalidation applies.
Next.js route
Prerenderable shellImmediate structure
Explicitly cached workReusable entry
Dynamic workRequest time
Reuse or revalidate cached entries

What Can Be Cached?

WorkCan it be cached?Current approach
Async server function resultYes, when suitable"use cache" with Cache Components
Server-side fetch()Yes, explicitlyforce-cache, next.revalidate, or a cached scope
Database-query functionYes, explicitlyWrap your own function in a cached scope
Route or componentYes, when suitable"use cache" with Cache Components
Cookies, headers, private request dataKeep dynamic by defaultRead at request time; isolate carefully
Static files and HTTP responsesSeparate concernBrowser/CDN/HTTP caching rules

Do not merge browser HTTP cache, CDN behavior, React render-time memoization, and the persistent Next.js server cache into one concept. They have different scopes and invalidation rules.

How Does fetch() Caching Work in Next.js 16?

By default, Next.js uses auto no cache. In development it fetches the remote resource on each request, although the Server Component HMR cache can reuse responses between hot reloads. During next build, an otherwise static route may fetch once while prerendering. If Dynamic APIs are detected, the default fetch runs for each request.

Choose fetch semantics explicitly
// Reuse a persistent Data Cache entry when fresh
const catalog = await fetch(url, { cache: 'force-cache' })

// Always reach the source for this request
const status = await fetch(url, { cache: 'no-store' })

// Cache with a maximum lifetime of one hour
const posts = await fetch(url, {
  next: { revalidate: 3600, tags: ['posts'] },
})

next.revalidate: 0 prevents caching. A positive number sets a cache lifetime in seconds. Do not combine cache: 'no-store' with a positive revalidate; those options conflict. Tags assigned through next.tags can be invalidated on demand.

Request Memoization Is Not Persistent Caching

React can memoize identical GET fetch calls with the same URL and options while one Server Component tree renders. If a layout, metadata function, and page request the same resource, that render-time deduplication can prevent repeated network work. The memoized value lasts only for the server render; it is not a persistent entry shared across later user requests and does not need on-demand revalidation.

Request memoization applies within the React component tree, not to Route Handlers. For a non-fetch data source, React's cache helper can deduplicate a function during a render, but that still differs from a durable "use cache" entry. When debugging, ask whether you are seeing same-render deduplication, development HMR reuse, or an explicitly persistent server cache.

Understanding "use cache"

With Cache Components enabled, the directive can mark an async route, component, or function as cacheable. Put it close to the smallest reusable work. A function's arguments and referenced serializable values participate in its cache identity.

A cached server function
import { cacheLife, cacheTag } from 'next/cache'

export async function getPublishedPosts() {
  'use cache'
  cacheLife('hours')
  cacheTag('posts')

  return queryPublishedPosts()
}

queryPublishedPosts() represents the application's own data function; it is not an invented ORM method. Read cookies or headers outside a normal cached scope and pass only safe values where appropriate. For most personal data, a dynamic boundary is easier to reason about.

Diagram 3: A "use cache" function callA Server Component calls a cached function. A valid entry returns directly. A missing or expired entry runs the source function, stores the result, and returns data.

Setting Cache Lifetime with cacheLife

cacheLife runs inside a "use cache" scope. Named profiles such as 'hours' communicate intent, while an object can configure stale, revalidate, and expire values in seconds.

Explicit custom lifetime
import { cacheLife } from 'next/cache'

export async function getDocumentationIndex() {
  'use cache'
  cacheLife({
    stale: 300,
    revalidate: 3600,
    expire: 86400,
  })
  return loadDocumentationIndex()
}

Stale controls how long the client may reuse an entry without checking the server. Revalidate controls when the server can serve the cached value and refresh it in the background. Expire is the maximum age before the next request must regenerate synchronously. Choose values from content requirements, not habit.

Diagram 4: Cache lifetime phasesA cached entry begins fresh, can enter a stale-while-revalidate phase, and eventually expires so the next request must wait for regeneration.
  1. FreshReuse
  2. Revalidate windowServe then refresh
  3. ExpiredRegenerate before serving

Tagging Cached Data with cacheTag

A tag describes a relationship across cache entries. A blog index and an author dashboard might both depend on the posts collection. Tagging both lets one mutation target that shared relationship without invalidating unrelated products or documentation.

Assign predictable tags inside cached work
import { cacheTag } from 'next/cache'

export async function getPost(slug: string) {
  'use cache'
  cacheTag('posts', `post-${slug}`)
  return findPublishedPostBySlug(slug)
}

Tags must be assigned before tag invalidation can affect an entry. Keep names predictable and free of secrets. Current limits are 256 characters per tag and 128 tags per entry.

Diagram 5: One tag can connect several cached viewsThe posts tag links cached blog index, recent posts, and author archive entries. Invalidating the tag targets the relationship rather than unrelated cached content.
Tag: postsBlog indexRecent postsAuthor archiveProducts and docs stay untouched

Revalidating by Path

revalidatePath invalidates cached data for a specific path. A concrete URL such as /blog/my-post targets one page. A dynamic pattern such as /blog/[slug] requires the 'page' or 'layout' type. Layout invalidation includes the selected layout and its descendants, so use it carefully.

Specific URL and route pattern
import { revalidatePath } from 'next/cache'

revalidatePath('/blog/my-post')
revalidatePath('/blog/[slug]', 'page')

In a Server Function, the affected path can update immediately when it is currently viewed. From a Route Handler, the path is marked and revalidated on the next visit. A path invalidation does not automatically refresh every other page sharing the same tagged data.

Diagram 6: Path revalidation scopeInvalidating the blog index refreshes that path. Other pages using the same data remain unchanged unless their path or shared cache tag is also invalidated.
revalidatePath('/blog')
/blogInvalidated
/dashboardUnchanged
/homeUnchanged

Revalidating by Tag: revalidateTag and updateTag

Next.js 16 separates eventual background freshness from immediate read-your-own-writes behavior:

  • revalidateTag('posts', 'max') works in Server Actions and Route Handlers. It marks matching entries stale, serves stale content when available, and refreshes in the background.
  • updateTag('posts') works only in Server Actions. It immediately expires matching entries so the user can see their own mutation without stale content.
NeedOften suitable
Refresh one routerevalidatePath
Refresh a shared data category in the backgroundrevalidateTag(tag, 'max')
Immediate read-your-own-writesupdateTag in a Server Action
Time-based cached-function lifetimecacheLife inside "use cache"
Always reach the source with fetchcache: 'no-store'
Diagram 7: Background and immediate tag invalidationAfter data changes, revalidateTag with the max profile permits a stale response followed by background refresh. updateTag in a Server Action expires the entry immediately for read-your-own-writes.

Revalidating After a Server Action

Connect invalidation to the protected mutation workflow from Blog #7. Validate, authenticate, authorize, and finish the database write first. Only then invalidate affected cache entries.

Mutation, revalidation, then redirect
'use server'

import { revalidatePath, updateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  let slug: string

  try {
    slug = await validateAuthorizeAndSavePost(formData)
  } catch {
    return { message: 'The post could not be saved.' }
  }

  revalidatePath('/blog')
  updateTag('posts')
  redirect(`/blog/${encodeURIComponent(slug)}`)
}

redirect() throws a framework-handled control-flow exception. Place it outside broad try/catch, after required revalidation. Do not revalidate on failed writes.

Caching Database Queries

A database client does not automatically gain persistent Next.js cache behavior. With Cache Components enabled, wrap your own query function in a "use cache" scope. The repository contains no database stack, so this example deliberately avoids invented Prisma, Drizzle, or SQL methods:

Cache an application-owned query function
import { cacheLife, cacheTag } from 'next/cache'
import { queryPublishedPosts } from '@/lib/posts'

export async function getPopularPosts() {
  'use cache'
  cacheLife('hours')
  cacheTag('posts', 'popular-posts')

  return queryPublishedPosts({ order: 'popular', limit: 6 })
}
Diagram 8: Database query with an explicit cacheA Server Component calls a cached query function. A cache hit returns data. A miss runs the database query, stores the result, and returns the data.

Mixing Cached and Dynamic Content

With Cache Components enabled, one product route can combine cached public details and reviews with a request-time cart behind a Suspense boundary. This is a server-rendering concern; it does not require turning the whole page into a Client Component. See the Server and Client Components guide for the component boundary.

Conceptual route composition
<ProductPage>
  <CachedProductDetails />
  <CachedReviewsSummary />
  <Suspense fallback={<CartSkeleton />}>
    <RequestTimeCart />
  </Suspense>
</ProductPage>

Layouts can widen path invalidation scope, as explained in our Next.js layouts and pages tutorial. Use revalidatePath(path, 'layout') only when descendants genuinely require invalidation.

Cache and Route Handlers

With Cache Components enabled, GET Route Handlers follow the same prerendering model as pages: cacheable work must be explicit and request-time work remains dynamic. Without Cache Components, current Next.js does not cache GET Route Handlers by default. A webhook or external mutation endpoint can call revalidateTag(tag, 'max') or revalidatePath after a successful write, but it cannot call updateTag, which is limited to Server Actions.

Do not add caching to authentication callbacks, webhooks, or request-specific endpoints merely because they use GET. Decide from freshness, privacy, and idempotency requirements, and keep authorization independent from cache state.

Be Careful Caching User-Specific Data

Caching is not an authorization boundary. A globally reusable key must never mix one user's account, cart, private messages, subscription details, medical records, or admin data with another user's response. Keep private request data dynamic by default unless you have a deliberate user-specific design and understand the relevant use cache: private constraints.

Public articles, products, and documentation entering a shared cache while account, messages, cart, and identity data remain separately protected
Reuse public data; isolate private data. A shared cache must never allow personalized information to cross user boundaries.
Diagram 9: Public and private cache decisionsPublic blog posts, product catalogs, and documentation can be good shared-cache candidates. Accounts, messages, carts, and dashboards require request-aware or carefully user-scoped handling.
Security rule

Never place secrets in public URLs or cache tags. A cache hit does not prove authorization. Perform permission checks at the protected data boundary even when a result may be cached.

Stale-While-Revalidate in Plain Language

The recommended revalidateTag(tag, 'max') behavior can return an available stale value promptly and refresh it in the background. The current request may see older content; a future request receives the refreshed entry. This is useful for public catalogs or blog posts where a brief delay is acceptable. It is not the behavior of every invalidation API—updateTag deliberately chooses immediate expiration.

Why Is My Next.js Page Showing Old Data?

First distinguish stale data from a failed request. If the operation throws, returns an unexpected status, or needs a protected fallback, continue with our Next.js 16 error handling guide.

  1. Confirm whether data is cached. Find force-cache, next.revalidate, "use cache", or a cache wrapper.
  2. Check the actual fetch options. Default, no-store, and force-cache do different work.
  3. Locate the cache boundary. A parent function or component may own the cached result.
  4. Verify the path. Paths are case-sensitive; patterns with dynamic segments require a type.
  5. Verify tag assignment. Invalidating posts cannot affect an entry that was never tagged posts.
  6. Confirm the mutation succeeded. Do not diagnose cache invalidation before proving the write committed.
  7. Use current syntax. The one-argument immediate-expiry form of revalidateTag is deprecated; prefer the 'max' profile or updateTag.
  8. Identify private data. A shared-cache design may be wrong for the content.
  9. Compare development and production. Development HMR can reuse fetch responses, while production prerendering has different timing.
  10. Separate cache layers. Browser, CDN, React render memoization, and Next.js server cache may each contribute.

Development and Production Behavior

Development is optimized for iteration, not for reproducing every production cache path. Server Component fetch responses are reused across Hot Module Replacement by default—even for no-store—until navigation or a full-page reload clears that HMR cache. A hard refresh with a cache-control: no-cache request header also causes fetch cache options to be ignored and reaches the source.

In a real Next.js project, validate production behavior with its configured scripts, commonly npm run build followed by npm run start. This repository has no Node project, so those commands are not available here.

Build a Cached Blog with Revalidation

A small blog feature can cache a published post list with cacheTag('posts'). An authorized admin action creates or edits a record, then calls updateTag('posts') when the editor must immediately see the result, plus revalidatePath('/blog') if the route output also needs targeted invalidation.

Read and write halves
// lib/posts.ts
import { cacheLife, cacheTag } from 'next/cache'

export async function getPublishedPosts() {
  'use cache'
  cacheLife('hours')
  cacheTag('posts')
  return queryPublishedPosts()
}

// app/admin/actions.ts
'use server'

import { revalidatePath, updateTag } from 'next/cache'

export async function publishPost(formData: FormData) {
  await validateAuthorizeAndPublish(formData)
  updateTag('posts')
  revalidatePath('/blog')
}

The functions named queryPublishedPosts and validateAuthorizeAndPublish are application boundaries, not fake database APIs. Implement them with the actual database and authentication stack in the Next.js project that consumes this pattern.

Common Next.js Caching Mistakes

  • Assuming every fetch is cached. The current default is auto no cache.
  • Copying Next.js 14 or 15 advice. Cache Components and the Next.js 16 tag APIs changed the recommended model.
  • Caching private data globally. Shared entries must never mix users.
  • Revalidating the wrong path. A page path and a layout pattern have different scopes.
  • Using tags without assigning them. Invalidation needs a matching tagged entry.
  • Using outdated revalidateTag(tag). Use a profile such as 'max' or migrate to updateTag.
  • Invalidating everything. Broad invalidation wastes useful cached work.
  • Never invalidating after writes. Successful mutations can leave stale UI.
  • Trusting development HMR behavior. Test a production build in a real Next.js app.
  • Confusing request memoization with persistent caching. Deduplication during one render is not a cross-request cache lifetime.
  • Adding no-store everywhere. It discards useful reuse even for safe stable content.
  • Caching everything. Mutable or personal data may require request-time work.

Next.js Caching Best Practices

  • Verify behavior against the exact installed Next.js version.
  • Cache only data that is safe and useful to reuse.
  • Keep private and request-specific information dynamic unless explicitly isolated.
  • Prefer precise invalidation over broad invalidation.
  • Use path revalidation for route-specific changes.
  • Use predictable tags for shared data relationships.
  • Use updateTag only in Server Actions requiring read-your-own-writes.
  • Use revalidateTag(tag, 'max') when stale-while-revalidate fits.
  • Invalidate only after a successful mutation.
  • Test production cache behavior.
  • Avoid unnecessary no-store.
  • Comment only non-obvious cache policy decisions.

Frequently Asked Questions

How does caching work in Next.js 16?

Next.js 16 can reuse explicitly cached server work and prerendered output. With opt-in Cache Components, use cache marks cacheable routes, components, or functions while uncached dynamic work runs at request time.

Is fetch cached by default in Next.js 16?

No. The default is auto no cache. Next.js may still fetch once during a production build when a route is statically prerendered, so choose force-cache, no-store, or revalidation options explicitly.

What is use cache in Next.js?

The use cache directive marks an async route, component, or function as cacheable. It is part of the opt-in Cache Components feature.

What are Cache Components?

Cache Components are an opt-in Next.js 16 model for combining a prerendered shell, explicitly cached work, and request-time dynamic content in one route.

What does revalidatePath do?

It invalidates cached data associated with a specific page or layout path. A route pattern with a dynamic segment also requires the page or layout type.

What is the difference between revalidatePath and revalidateTag?

revalidatePath targets a route path. revalidateTag targets all cached entries carrying a tag and, with the recommended max profile, uses stale-while-revalidate behavior.

What is updateTag in Next.js?

updateTag is a Server-Action-only API that immediately expires tagged data for read-your-own-writes behavior after a mutation.

How do I refresh cached data after a Server Action?

After a successful write, use a precise path or tag API: revalidatePath for route output, updateTag for immediate tagged freshness, or revalidateTag with max when background refresh is acceptable.

Can I cache database queries in Next.js?

Yes. With Cache Components enabled, wrap your own database query function in a use cache scope and optionally apply cacheLife and cacheTag. Database libraries are not automatically persistent Next.js caches.

Should user-specific data be cached?

Only with deliberate user-specific scope and a documented need. Keep private request data dynamic by default, and never place it in a shared cache that another user could reuse.

Official Resources

Next Steps

You can now distinguish dynamic work from explicit cached work, choose a lifetime, connect related entries with tags, and invalidate only what a successful mutation changed. Continue with Next.js 16 error handling to make failed reads and writes safe for users.

WhatsApp