Next.js 09 ▲ Revalidating — Time-based & on-demand cache invalidation
Revalidation is the process of updating cached data so that users see fresh content without losing the performance benefit of caching. Next.js offers two strategies that serve different purposes: time-based revalidation, which refreshes cached data automatically after a set duration using cacheLife, and on-demand revalidation, which invalidates specific cached entries manually after a mutation using revalidateTag, updateTag, or revalidatePath . Neither strategy is universally correct. The choice depends on whether your application can observe when data changes, and how quickly users need to see the result of their own writes.
This chapter covers both strategies in the Cache Components model. It explains how cacheLife profiles control the stale-while-revalidate window, how cacheTag labels cached data for targeted invalidation, and how the three on-demand functions differ in scope and semantics. It also covers the practical decision between revalidateTag and updateTag: the former serves stale content while fresh data loads in the background, while the latter blocks the next request until fresh data is ready, which is what read-your-own-writes scenarios require .
Key point: revalidateTag(tag, 'max') uses stale-while-revalidate semantics—the next visitor sees stale content while fresh data loads in the background. updateTag(tag) expires the entry immediately—the next request waits for fresh data. Use updateTag in Server Actions when the user must see their own change.
Why two revalidation strategies exist
The freshness problem. A cached page is fast but potentially outdated. A page that regenerates on every request is fresh but slow. Time-based revalidation balances the two: the cached content is served until its age exceeds the revalidate duration, at which point a background regeneration begins while the stale content continues to be served . This is the stale-while-revalidate pattern, and it works well for content where a slight delay is acceptable, such as blog posts or product catalogs .
The mutation problem. When a user submits a form and creates a post, they expect to see that post immediately. Waiting for a time-based revalidation window would show them the old state. On-demand revalidation solves this by invalidating the specific cache entries affected by the mutation, so the next request regenerates fresh content . The scope of invalidation matters: revalidatePath targets one page or layout, while revalidateTag targets data across every route that uses the same tag .
The read-your-own-writes problem. Even on-demand revalidation has a subtlety. With stale-while-revalidate semantics, the user who just made a change may see the stale content one more time while the fresh data loads in the background. For a “create post” flow that redirects to the new post, this is unacceptable. The updateTag function exists for exactly this case: it expires the entry immediately, and the next request blocks until fresh data is ready .
a. Time-based revalidation with cacheLife
cacheLife controls how long cached data remains valid. It is used inside a 'use cache' scope and accepts either a built-in profile name or a custom configuration object .
import { cacheLife } from 'next/cache'
export async function getProducts() {
'use cache'
cacheLife('hours')
return db.query('SELECT * FROM products')
}
The built-in profiles define three values: stale, the duration during which the client considers the data fresh; revalidate, the duration after which the server regenerates the cache entry in the background; and expire, the maximum age before the entry is considered expired and must be regenerated synchronously . The 'hours' profile sets stale: 5m, revalidate: 1h, and expire: 1d. The 'max' profile sets revalidate: 30d and expire: 1y, which is useful for content that changes rarely and should be invalidated only on demand .
A cache is considered “short-lived” when it uses the 'seconds' profile, sets revalidate: 0, or has an expire under five minutes. Short-lived caches are excluded from prerenders and become dynamic holes instead, because they would be stale almost immediately after the build .
b. Tagging data with cacheTag
cacheTag labels cached data so that it can be invalidated on demand. Like cacheLife, it is used inside a 'use cache' scope .
import { cacheTag } from 'next/cache'
export async function getProducts() {
'use cache'
cacheTag('products')
return db.query('SELECT * FROM products')
}
A single cached scope can have multiple tags. A product query might carry product:123 for the specific product and products:list for every list that includes it, allowing an action to invalidate at whichever level it changed . Tags are case-sensitive and must not exceed 256 characters .
Tags can also be attached to fetch requests using the next.tags option, which is the pattern for cached external API calls outside a 'use cache' scope .
c. revalidateTag: stale-while-revalidate invalidation
revalidateTag marks cached entries with the given tag as stale. The next visit to any route that uses that tag serves the stale content while fresh data loads in the background .
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, data: ProductData) {
await db.product.update({ where: { id }, data })
revalidateTag('products', 'max')
}
The second argument is required in Next.js 16. The single-argument form is deprecated and currently behaves like updateTag (immediate expiration) . The recommended value is 'max', which provides the longest stale window. Any built-in or custom cacheLife profile can be passed instead to control the stale-while-revalidate behavior more precisely .
revalidateTag can be called in Server Actions and Route Handlers. It cannot be called in Client Components or Middleware .
d. updateTag: immediate expiration for read-your-own-writes
updateTag immediately expires the cached data for a tag. The next request waits for fresh data rather than serving stale content, which ensures that the user who made a change sees it right away .
'use server'
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const post = await db.post.create({
data: { title: formData.get('title'), content: formData.get('content') },
})
updateTag('posts')
redirect(`/posts/${post.id}`)
}
updateTag can only be called from within a Server Action. It cannot be used in Route Handlers, Client Components, or any other context . This restriction exists because the read-your-own-writes guarantee depends on the action running in the same request cycle as the redirect or re-render.
e. revalidatePath: invalidating a route
revalidatePath invalidates a specific page or layout path. It is useful when one record maps cleanly to one page, and adding tags to the underlying fetch would be unnecessary overhead .
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePost(id: string, data: PostData) {
await db.post.update({ where: { id }, data })
revalidatePath(`/posts/${id}`)
}
The function accepts a literal path like /posts/123 or a route pattern like /posts/[id]. For a route pattern, a second argument of 'page' or 'layout' is required. Passing 'layout' invalidates the layout at that segment, all nested layouts beneath it, and all pages beneath them . Using revalidatePath('/') with 'layout' invalidates the root layout and everything below it—this is a whole-site operation and should not be used for a local change .
revalidatePath and revalidateTag are complementary. A single mutation may need both: revalidatePath to refresh the page the user is on, and revalidateTag to refresh other pages that display the same data .
Complete Example Session
// ============================================
// PART 1: TIME-BASED REVALIDATION WITH cacheLife
// ============================================
// app/lib/data.ts
import { cacheLife } from 'next/cache'
export async function getProducts() {
'use cache'
cacheLife('hours')
return db.query('SELECT * FROM products')
}
// After 1 hour, the next request serves stale data
// while fresh data loads in the background.
// ============================================
// PART 2: CUSTOM cacheLife CONFIGURATION
// ============================================
export async function getStats() {
'use cache'
cacheLife({
stale: 3600, // 1 hour until considered stale
revalidate: 7200, // 2 hours until revalidated
expire: 86400, // 1 day until expired
})
return fetch('https://api.example.com/stats').then((r) => r.json())
}
// ============================================
// PART 3: TAGGING CACHED DATA
// ============================================
import { cacheTag } from 'next/cache'
export async function getProduct(id: string) {
'use cache'
cacheTag(`product:${id}`, 'products')
return db.product.findUnique({ where: { id } })
}
// Two tags: one for the specific product,
// one for every product list that includes it.
// ============================================
// PART 4: revalidateTag WITH STALE-WHILE-REVALIDATE
// ============================================
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, data: ProductData) {
await db.product.update({ where: { id }, data })
revalidateTag(`product:${id}`, 'max')
}
// The next visitor sees stale content briefly
// while fresh data loads in the background.
// ============================================
// PART 5: updateTag FOR READ-YOUR-OWN-WRITES
// ============================================
'use server'
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const post = await db.post.create({ data: { title: formData.get('title') } })
updateTag('posts')
redirect(`/posts/${post.id}`)
}
// The next request blocks until fresh data is ready.
// The user sees their own post immediately.
// ============================================
// PART 6: revalidatePath FOR A SINGLE PAGE
// ============================================
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePost(id: string, data: PostData) {
await db.post.update({ where: { id }, data })
revalidatePath(`/posts/${id}`)
}
// Only the specific post page is invalidated.
// Other pages using the same data keep their cache.
// ============================================
// PART 7: revalidatePath WITH A LAYOUT
// ============================================
revalidatePath('/posts', 'layout')
// Invalidates the /posts layout, all nested layouts,
// and all pages beneath them.
// Broad scope—use for section-wide changes.
// ============================================
// PART 8: COMBINING PATH AND TAG
// ============================================
export async function publishPost(id: string) {
await db.post.update({ where: { id }, data: { published: true } })
revalidatePath(`/posts/${id}`) // the post page
revalidateTag('posts', 'max') // all post lists
}
// The user sees the updated post page immediately.
// Other pages with post lists refresh in the background.
// ============================================
// PART 9: ROUTE HANDLER FOR EXTERNAL WEBHOOKS
// ============================================
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const { tag } = await request.json()
revalidateTag(tag, 'max')
return Response.json({ revalidated: true })
}
// revalidateTag works in Route Handlers.
// updateTag does not—it is Server Actions only.
// ============================================
// PART 10: DECISION GUIDE
// ============================================
// Time-based: cacheLife('hours')
// → data changes unpredictably, slight delay OK
//
// revalidateTag(tag, 'max')
// → mutation affects data across multiple routes
// → stale-while-revalidate is acceptable
//
// updateTag(tag)
// → Server Action only
// → user must see their own change immediately
//
// revalidatePath(path)
// → one record maps to one page
// → no tags needed
The ten parts covered time-based revalidation, custom cacheLife, tagging, revalidateTag with stale-while-revalidate, updateTag for read-your-own-writes, revalidatePath for a single page, layout invalidation, combining path and tag, Route Handler usage, and the decision guide.
Quick Reference
The three on-demand functions
| Function | Context | Semantics | Use case |
|---|---|---|---|
revalidateTag(tag, 'max') | Server Actions, Route Handlers | Stale-while-revalidate | Background refresh, acceptable delay |
updateTag(tag) | Server Actions only | Immediate expiration, next request blocks | Read-your-own-writes |
revalidatePath(path) | Server Actions, Route Handlers | Path-level invalidation | One record, one page |
cacheLife profiles
| Profile | stale | revalidate | expire |
|---|---|---|---|
default | 5m | 15m | never |
seconds | 30s | 1s | 60s |
minutes | 5m | 1m | 1h |
hours | 5m | 1h | 1d |
days | 5m | 1d | 1w |
weeks | 5m | 1w | 30d |
max | 5m | 30d | 1y |
Tag patterns
| Pattern | Example | Scope |
|---|---|---|
| Per-record | product:${id} | One entity |
| List | posts:list | All lists containing posts |
| Shared | navigation | Site-wide shared data |
| Locale-aware | posts:list:en | One locale variant |
revalidatePath types
| Call | Invalidates |
|---|---|
revalidatePath('/posts/123') | That page only |
revalidatePath('/posts/[id]', 'page') | All matching pages |
revalidatePath('/posts', 'layout') | Layout, nested layouts, all pages below |
revalidatePath('/', 'layout') | Entire site (avoid for local changes) |
Best Practices
✅ Do This:
// Use updateTag in Server Actions when the user must see their change
updateTag('posts') // ✅
// Use revalidateTag with 'max' for background refresh
revalidateTag('products', 'max') // ✅
// Tag cached data with specific and broad tags
cacheTag(`product:${id}`, 'products') // ✅
// Use revalidatePath when one record maps to one page
revalidatePath(`/posts/${id}`) // ✅
// Combine path and tag for comprehensive invalidation
revalidatePath(`/posts/${id}`); revalidateTag('posts', 'max') // ✅
❌ Don’t Do This:
// Use the deprecated single-argument form
revalidateTag('posts') // ❌ (deprecated)
// Use updateTag in a Route Handler
updateTag('posts') // in a webhook // ❌ (Server Actions only)
// Invalidate the entire site for a local change
revalidatePath('/', 'layout') // ❌
// Forget to tag cached data that will need invalidation
'use cache'; return db.query(...) // no cacheTag // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
revalidateTag single argument deprecation | Next.js 16 requires a profile | Add 'max': revalidateTag('posts', 'max') |
updateTag fails in Route Handler | Only available in Server Actions | Use revalidateTag in Route Handlers |
| Cache not invalidated after mutation | No tag attached or no revalidation call | Add cacheTag and call revalidateTag or updateTag |
| User sees stale content after creating | revalidateTag uses stale-while-revalidate | Use updateTag for read-your-own-writes |
| Other pages still show old data | revalidatePath only affects the specified path | Add revalidateTag for shared data |
| Whole site refreshes unexpectedly | revalidatePath('/', 'layout') used | Use a specific path or tag |
Real-World Examples
1. Time-based revalidation
'use cache'
cacheLife('hours')
2. Custom lifetime
cacheLife({ stale: 3600, revalidate: 7200, expire: 86400 })
3. Tag a query
'use cache'
cacheTag('products')
4. revalidateTag with max
revalidateTag('products', 'max')
5. updateTag for immediate consistency
updateTag('posts')
6. revalidatePath for one page
revalidatePath(`/posts/${id}`)
7. revalidatePath for a layout
revalidatePath('/posts', 'layout')
8. Combine path and tag
revalidatePath(`/posts/${id}`)
revalidateTag('posts', 'max')
9. Route Handler with revalidateTag
revalidateTag(tag, 'max')
10. Tag pattern for locale
cacheTag(`posts:list:${locale}`)
Visual
Time-based vs on-demand
┌─────────────────────────────────────────────────────────────┐
│ TIME-BASED │
│ │
│ Cache entry created │
│ │ │
│ ▼ │
│ revalidate duration passes │
│ │ │
│ ▼ │
│ Next request serves stale │
│ Background regeneration starts │
│ │ │
│ ▼ │
│ Fresh entry replaces stale │
│ │
│ ON-DEMAND │
│ │
│ Mutation occurs │
│ │ │
│ ▼ │
│ revalidateTag / updateTag / revalidatePath │
│ │ │
│ ▼ │
│ Entry marked stale or expired │
│ │ │
│ ▼ │
│ Next request gets fresh (or stale-then-fresh) │
└─────────────────────────────────────────────────────────────┘
revalidateTag vs updateTag
┌─────────────────────────────────────────────────────────────┐
│ revalidateTag(tag, 'max') │
│ │
│ Entry marked stale │
│ │ │
│ ▼ │
│ Next request: serves STALE immediately │
│ │ │
│ ▼ │
│ Background fetch of fresh data │
│ │ │
│ ▼ │
│ Subsequent requests: FRESH │
│ │
│ ───────────────────────────────────────────────────────────│
│ │
│ updateTag(tag) │
│ │
│ Entry expired immediately │
│ │ │
│ ▼ │
│ Next request: WAITS for fresh data │
│ │ │
│ ▼ │
│ User sees their change right away │
└─────────────────────────────────────────────────────────────┘
Scope of invalidation
┌─────────────────────────────────────────────────────────────┐
│ SCOPE │
│ │
│ revalidatePath('/posts/123') │
│ └── Only /posts/123 │
│ │
│ revalidateTag('posts', 'max') │
│ ├── /posts (list) │
│ ├── /dashboard (recent posts) │
│ └── /users/alice/posts (author page) │
│ │
│ revalidatePath('/posts', 'layout') │
│ ├── /posts layout │
│ ├── /posts/[id] (all post pages) │
│ └── nested layouts and pages │
│ │
│ revalidatePath('/', 'layout') │
│ └── Entire site (avoid for local changes) │
└─────────────────────────────────────────────────────────────┘
Decision flow
┌─────────────────────────────────────────────────────────────┐
│ WHICH FUNCTION? │
│ │
│ Is this a Server Action? │
│ │ │
│ ├── Yes ──► Does the user need to see their change │
│ │ immediately? │
│ │ │ │
│ │ ├── Yes ──► updateTag(tag) │
│ │ │ │
│ │ └── No ──► revalidateTag(tag, 'max') │
│ │ │
│ └── No (Route Handler) ──► revalidateTag(tag, 'max') │
│ │
│ Does one record map to one page? │
│ │ │
│ └── Yes ──► revalidatePath(path) │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Time-based control | cacheLife(profile) inside 'use cache' |
| Tagging for invalidation | cacheTag(tag) inside 'use cache' |
| Stale-while-revalidate | revalidateTag(tag, 'max') |
| Immediate expiration | updateTag(tag) (Server Actions only) |
| Path invalidation | revalidatePath(path) |
| Layout invalidation | revalidatePath(path, 'layout') |
| Default revalidateTag form | Two arguments required in Next.js 16 |
| updateTag context | Server Actions only |
| revalidateTag context | Server Actions and Route Handlers |
Key takeaways:
- Time-based revalidation is automatic; on-demand is explicit.
cacheLiferefreshes cached data after a duration.revalidateTag,updateTag, andrevalidatePathinvalidate specific entries when data changes . revalidateTaguses stale-while-revalidate;updateTagblocks. The former serves stale content while fresh data loads, which is fine for background refresh. The latter expires immediately so the next request waits for fresh data, which is required for read-your-own-writes .updateTagis Server Actions only. Route Handlers must userevalidateTag. The restriction exists because the read-your-own-writes guarantee depends on the action running in the same request cycle as the redirect or re-render .- Tags are the mechanism for cross-route invalidation. A single tag can invalidate data on every page that uses it. Tag patterns like
product:${id}for a specific record andproductsfor all lists allow invalidation at the appropriate granularity . revalidatePathis for one-record-one-page mappings. When a change affects a single page and no other route uses the same data,revalidatePathis simpler than adding a tag to the underlying fetch .- The single-argument
revalidateTagis deprecated. Next.js 16 requires a second argument, with'max'as the recommended value for stale-while-revalidate semantics .
Remember: Revalidation is the bridge between caching and freshness. Time-based revalidation keeps cached content within an acceptable age without any application logic. On-demand revalidation keeps content consistent with mutations. The choice between revalidateTag and updateTag is not about performance—it is about what the user sees. If a user creates a post and is redirected to it, they must see the post, not a cached list without it. That is updateTag. If a background job updates a product catalog, the next visitor can see the old catalog for a moment while the new one loads. That is revalidateTag. Get the semantics right, and the cache becomes an asset instead of a source of confusion.
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!