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.
Fresh request
- Request
- Database or API
- New result
- Store in cache
- User
Later request
- Request
- Cache check
- Valid entry
- Cached result

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.
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.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfigWhat Can Be Cached?
| Work | Can it be cached? | Current approach |
|---|---|---|
| Async server function result | Yes, when suitable | "use cache" with Cache Components |
Server-side fetch() | Yes, explicitly | force-cache, next.revalidate, or a cached scope |
| Database-query function | Yes, explicitly | Wrap your own function in a cached scope |
| Route or component | Yes, when suitable | "use cache" with Cache Components |
| Cookies, headers, private request data | Keep dynamic by default | Read at request time; isolate carefully |
| Static files and HTTP responses | Separate concern | Browser/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.
// 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.
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.
"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.
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.
- FreshReuse
- Revalidate windowServe then refresh
- 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.
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.
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.
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.
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.
| Need | Often suitable |
|---|---|
| Refresh one route | revalidatePath |
| Refresh a shared data category in the background | revalidateTag(tag, 'max') |
| Immediate read-your-own-writes | updateTag in a Server Action |
| Time-based cached-function lifetime | cacheLife inside "use cache" |
Always reach the source with fetch | cache: 'no-store' |
revalidateTag(tag, 'max')
- Mark stale
- Serve stale if available
- Refresh in background
- Future request is fresh
updateTag(tag)
- Expire immediately
- Read updated source
- User sees own write
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.
'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:
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 })
}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.
<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 data
- Blog posts
- Product catalog
- Documentation
Private data
- User account
- Messages
- Personal dashboard
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.
- Confirm whether data is cached. Find
force-cache,next.revalidate,"use cache", or a cache wrapper. - Check the actual fetch options. Default,
no-store, andforce-cachedo different work. - Locate the cache boundary. A parent function or component may own the cached result.
- Verify the path. Paths are case-sensitive; patterns with dynamic segments require a type.
- Verify tag assignment. Invalidating
postscannot affect an entry that was never taggedposts. - Confirm the mutation succeeded. Do not diagnose cache invalidation before proving the write committed.
- Use current syntax. The one-argument immediate-expiry form of
revalidateTagis deprecated; prefer the'max'profile orupdateTag. - Identify private data. A shared-cache design may be wrong for the content.
- Compare development and production. Development HMR can reuse fetch responses, while production prerendering has different timing.
- 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.
// 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 toupdateTag. - 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-storeeverywhere. 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
updateTagonly 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.js Cache Components guide
- Next.js Revalidating guide
- Next.js server-side
fetchreference - Next.js
use cachedirective - Next.js
cacheLifereference - Next.js
cacheTagreference - Next.js
revalidatePathreference - Next.js
revalidateTagreference - Next.js
updateTagreference
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.
