Time-based revalidation is great, but what if you need to update cached data immediately when content changes? That's where on-demand revalidation comes in. With revalidatePath() and revalidateTag(), you can programmatically invalidate specific cached data whenever you need—like when a blog post is published, a product is updated, or any content changes. Combined with cache tags, you get precise control over what gets revalidated and when. Let's master on-demand revalidation!
The Problem with Time-Based Revalidation
❌ Time-Based Revalidation Limitation
async function getBlogPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }, // Revalidate every hour
});
return res.json();
}
// Scenario:
// 1. Post fetched at 10:00 AM, cached for 1 hour
// 2. You update the post at 10:05 AM
// 3. Users see OLD content until 11:00 AM! ❌
// Problem: You must wait for revalidation time to expire
// Can't update immediately when content changes✅ On-Demand Revalidation Solution
// Fetch with cache tag
async function getBlogPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
revalidate: 3600,
tags: ['posts', `post-${slug}`], // Cache tags
},
});
return res.json();
}
// When post is updated:
import { revalidateTag } from 'next/cache';
async function updatePost(slug: string, data: any) {
// Update in database
await db.posts.update({ where: { slug }, data });
// Immediately revalidate this post's cache
revalidateTag(`post-${slug}`);
// User sees new content instantly! ✅
}
// ✅ Content updates immediately
// ✅ No waiting for revalidation time
// ✅ Precise control over what's revalidatedrevalidatePath() - Path-Based Revalidation
Revalidate all data for a specific path:
Basic Usage
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Save to database
await db.posts.create({
data: { title, content },
});
// Revalidate the blog page
revalidatePath('/blog');
// Now /blog shows the new post immediately!
}
// ✅ Revalidates /blog page
// ✅ New post appears instantly
// ✅ No waiting for time-based revalidationRevalidate Specific Page
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function updatePost(slug: string, data: any) {
// Update post in database
await db.posts.update({
where: { slug },
data,
});
// Revalidate the specific post page
revalidatePath(`/blog/${slug}`);
// Redirect to the updated post
redirect(`/blog/${slug}`);
}
// ✅ Only revalidates /blog/my-post
// ✅ Other pages unaffected
// ✅ Efficient and preciseRevalidate Multiple Paths
'use server';
import { revalidatePath } from 'next/cache';
export async function publishPost(slug: string) {
// Publish post
await db.posts.update({
where: { slug },
data: { published: true },
});
// Revalidate multiple paths
revalidatePath('/blog'); // Blog list
revalidatePath(`/blog/${slug}`); // Specific post
revalidatePath('/'); // Home page (if it shows recent posts)
// All affected pages update instantly!
}
// ✅ Revalidates all related pages
// ✅ Ensures consistency across sitePath Type Options
import { revalidatePath } from 'next/cache';
// Option 1: Revalidate single page (default)
revalidatePath('/blog', 'page');
// Only revalidates /blog
// /blog/post-1, /blog/post-2 NOT revalidated
// Option 2: Revalidate all nested routes (layout)
revalidatePath('/blog', 'layout');
// Revalidates /blog AND all nested:
// /blog, /blog/post-1, /blog/post-2, /blog/category/tech, etc.
// Common usage:
revalidatePath('/blog', 'layout'); // Revalidate entire blog sectionrevalidateTag() - Tag-Based Revalidation
Revalidate all data with a specific cache tag:
Step 1: Tag Your Data
// Add tags to your fetch requests
export async function getBlogPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
revalidate: 3600,
tags: ['posts'], // Tag for all posts
},
});
return res.json();
}
export async function getBlogPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
revalidate: 3600,
tags: ['posts', `post-${slug}`], // Multiple tags
},
});
return res.json();
}
export async function getPostsByCategory(category: string) {
const res = await fetch(`https://api.example.com/posts?category=${category}`, {
next: {
revalidate: 3600,
tags: ['posts', `category-${category}`], // Category-specific tag
},
});
return res.json();
}
// ✅ Each fetch has relevant tags
// ✅ Can revalidate by specific tag
// ✅ Flexible and preciseStep 2: Revalidate by Tag
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(data: any) {
// Create post
await db.posts.create({ data });
// Revalidate all data tagged with 'posts'
revalidateTag('posts');
// This revalidates:
// - Blog post list (/blog)
// - Individual posts (/blog/post-1, /blog/post-2)
// - Category pages (/blog/category/tech)
// All because they're tagged with 'posts'!
}
export async function updatePost(slug: string, data: any) {
// Update post
await db.posts.update({ where: { slug }, data });
// Revalidate only this specific post
revalidateTag(`post-${slug}`);
// Only pages with this specific tag are revalidated
// More efficient than revalidating all posts
}
export async function deletePostFromCategory(slug: string, category: string) {
// Delete post
await db.posts.delete({ where: { slug } });
// Revalidate category page
revalidateTag(`category-${category}`);
// Only this category's data is revalidated
}
// ✅ Precise control over what's revalidated
// ✅ Efficient - only revalidates what changed
// ✅ Works across multiple pagesComplete Example: Blog with On-Demand Revalidation
1. Data Layer with Cache Tags
// Fetch functions with cache tags
export async function getAllPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
revalidate: 3600, // Cache for 1 hour
tags: ['posts'], // Tag: all posts
},
});
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
revalidate: 3600,
tags: ['posts', `post-${slug}`], // Tags: all posts + specific post
},
});
if (!res.ok) throw new Error('Failed to fetch post');
return res.json();
}
export async function getFeaturedPosts() {
const res = await fetch('https://api.example.com/posts/featured', {
next: {
revalidate: 3600,
tags: ['posts', 'featured-posts'], // Tags: all posts + featured
},
});
if (!res.ok) throw new Error('Failed to fetch featured posts');
return res.json();
}
// ✅ All fetch functions tagged appropriately
// ✅ Can revalidate by 'posts' to refresh everything
// ✅ Can revalidate specific post or featured posts2. Server Actions with Revalidation
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const slug = formData.get('slug') as string;
const content = formData.get('content') as string;
// Validate
if (!title || !slug || !content) {
throw new Error('Missing required fields');
}
// Create post in database
await db.posts.create({
data: {
title,
slug,
content,
published: false,
},
});
// Revalidate blog pages
revalidatePath('/blog');
// Redirect to the new post
redirect(`/blog/${slug}`);
}
export async function updatePost(slug: string, formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Update post in database
await db.posts.update({
where: { slug },
data: { title, content },
});
// Revalidate this specific post
revalidateTag(`post-${slug}`);
// Also revalidate blog list (title might have changed)
revalidatePath('/blog');
// Redirect to updated post
redirect(`/blog/${slug}`);
}
export async function publishPost(slug: string) {
// Publish post
await db.posts.update({
where: { slug },
data: { published: true, publishedAt: new Date() },
});
// Revalidate everything related to posts
revalidateTag('posts'); // Revalidates all pages tagged with 'posts'
// Success!
return { success: true };
}
export async function deletePost(slug: string) {
// Delete post
await db.posts.delete({
where: { slug },
});
// Revalidate blog pages
revalidatePath('/blog');
revalidateTag('posts');
// Redirect to blog list
redirect('/blog');
}
export async function toggleFeatured(slug: string, featured: boolean) {
// Update featured status
await db.posts.update({
where: { slug },
data: { featured },
});
// Revalidate featured posts
revalidateTag('featured-posts');
// Also revalidate the specific post
revalidateTag(`post-${slug}`);
return { success: true };
}
// ✅ Each action revalidates appropriate caches
// ✅ Precise control over what updates
// ✅ Users see changes immediately3. Blog Pages Using Tagged Data
import { getAllPosts, getFeaturedPosts } from '@/lib/blog';
// Blog list page
export default async function BlogPage() {
// Both calls use cached data with 'posts' tag
const [posts, featured] = await Promise.all([
getAllPosts(),
getFeaturedPosts(),
]);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
{/* Featured posts */}
<section className="mb-12">
<h2 className="text-2xl font-bold mb-4">Featured</h2>
<div className="grid grid-cols-3 gap-6">
{featured.map(post => (
<FeaturedPostCard key={post.id} post={post} />
))}
</div>
</section>
{/* All posts */}
<section>
<h2 className="text-2xl font-bold mb-4">All Posts</h2>
<div className="space-y-6">
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
</section>
</div>
);
}
// ✅ When revalidateTag('posts') is called:
// - getAllPosts() refreshes
// - getFeaturedPosts() refreshes
// - Page shows new data immediatelyimport { getPost } from '@/lib/blog';
import { notFound } from 'next/navigation';
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<article className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="text-gray-600 mb-8">
Published on {new Date(post.publishedAt).toLocaleDateString()}
</div>
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
);
}
// ✅ When revalidateTag(`post-${slug}`) is called:
// - Only this specific post refreshes
// - Other posts unaffected
// - Efficient and preciseAPI Route for Revalidation
Create an API endpoint for external systems to trigger revalidation:
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
// Verify secret token (for security)
const authHeader = request.headers.get('authorization');
const secret = authHeader?.replace('Bearer ', '');
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
const body = await request.json();
const { type, value } = body;
try {
if (type === 'path') {
// Revalidate by path
revalidatePath(value);
return NextResponse.json({
revalidated: true,
type: 'path',
value,
now: Date.now(),
});
}
if (type === 'tag') {
// Revalidate by tag
revalidateTag(value);
return NextResponse.json({
revalidated: true,
type: 'tag',
value,
now: Date.now(),
});
}
return NextResponse.json(
{ error: 'Invalid type. Use "path" or "tag"' },
{ status: 400 }
);
} catch (error) {
return NextResponse.json(
{ error: 'Error revalidating' },
{ status: 500 }
);
}
}
// Usage from external systems (e.g., CMS webhook):
// POST https://yoursite.com/api/revalidate
// Headers: { Authorization: 'Bearer YOUR_SECRET' }
// Body: { type: 'tag', value: 'posts' }
// ✅ Secure with secret token
// ✅ Supports both path and tag revalidation
// ✅ Can be called from webhooks, CMS, etc.Using the Revalidation API
// From a CMS webhook or external system
async function triggerRevalidation() {
await fetch('https://yoursite.com/api/revalidate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.REVALIDATION_SECRET}`,
},
body: JSON.stringify({
type: 'tag',
value: 'posts',
}),
});
}
// From your CMS:
// 1. Post is updated
// 2. CMS calls your revalidation API
// 3. Cache is cleared
// 4. Next request gets fresh data
// ✅ Integrates with external systems
// ✅ Instant cache updates
// ✅ No manual intervention neededOn-Demand Revalidation Structure
Project structure with revalidation API and Server Actions
Select a file or folder to see details
Revalidation Best Practices
1. Use Descriptive Tag Names
// ✅ GOOD: Descriptive, clear tags
tags: ['posts', 'post-123', 'category-tech', 'author-john']
// ❌ BAD: Vague, unclear tags
tags: ['data', 'page', 'content']
// ✅ GOOD: Hierarchical tags
tags: ['blog-posts', 'blog-post-how-to-code', 'blog-category-programming']
// Makes it clear what each tag represents2. Tag Strategically
// ✅ GOOD: Multiple tags for flexibility
async function getPost(slug: string) {
return fetch(`/api/posts/${slug}`, {
next: {
tags: [
'posts', // All posts
`post-${slug}`, // This specific post
`author-${authorId}`, // Posts by this author
`category-${cat}`, // Posts in this category
],
},
});
}
// Can revalidate at different granularities:
// - revalidateTag('posts') → All posts
// - revalidateTag('post-my-slug') → One post
// - revalidateTag('author-123') → All by author
// - revalidateTag('category-tech') → All in category3. Revalidate Related Data
// ✅ GOOD: Revalidate all related caches
async function updatePost(slug: string, data: any) {
await db.posts.update({ where: { slug }, data });
// Revalidate specific post
revalidateTag(`post-${slug}`);
// Revalidate list pages
revalidatePath('/blog');
// Revalidate home page (if it shows recent posts)
revalidatePath('/');
}
// ❌ BAD: Forgot to revalidate related pages
async function updatePost(slug: string, data: any) {
await db.posts.update({ where: { slug }, data });
revalidateTag(`post-${slug}`);
// Post updates but list doesn't show changes!
}4. Combine Time-Based and On-Demand
// Best of both worlds
async function getBlogPosts() {
return fetch('https://api.example.com/posts', {
next: {
revalidate: 3600, // Time-based: refresh every hour
tags: ['posts'], // On-demand: can refresh immediately
},
});
}
// Benefits:
// ✅ Automatic refresh every hour (safety net)
// ✅ Immediate refresh when content changes (on-demand)
// ✅ Best user experience5. Secure Your Revalidation API
// ✅ GOOD: Verify secret token
const authHeader = request.headers.get('authorization');
const secret = authHeader?.replace('Bearer ', '');
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// ❌ BAD: No security
// Anyone can trigger revalidation!
// Security is critical - prevent abuseCommon Revalidation Patterns
Pattern 1: Content Management
// When content is created, updated, or deleted
async function manageContent(action: 'create' | 'update' | 'delete', data: any) {
switch (action) {
case 'create':
await db.posts.create({ data });
revalidatePath('/blog');
revalidateTag('posts');
break;
case 'update':
await db.posts.update({ where: { id: data.id }, data });
revalidateTag(`post-${data.slug}`);
revalidatePath('/blog');
break;
case 'delete':
await db.posts.delete({ where: { id: data.id } });
revalidatePath('/blog');
revalidateTag('posts');
break;
}
}Pattern 2: Category/Tag Updates
async function updateCategory(slug: string, data: any) {
await db.categories.update({ where: { slug }, data });
// Revalidate category page
revalidatePath(`/blog/category/${slug}`);
// Revalidate all posts in this category
revalidateTag(`category-${slug}`);
// Revalidate category list
revalidatePath('/blog/categories');
}Pattern 3: User Actions
async function likePost(postId: string, userId: string) {
await db.likes.create({ data: { postId, userId } });
// Revalidate this post (like count changed)
revalidateTag(`post-${postId}`);
}
async function addComment(postId: string, comment: string) {
await db.comments.create({ data: { postId, comment } });
// Revalidate this post (new comment)
revalidateTag(`post-${postId}`);
}Key Takeaways
- revalidatePath() - revalidate specific paths or entire sections
- revalidateTag() - revalidate all data with specific tag
- Cache tags - add tags to fetch for granular control
- On-demand - update cache immediately when content changes
- Server Actions - call revalidation functions in actions
- API routes - expose revalidation endpoint for webhooks
- Combine strategies - time-based + on-demand for best results
- Security - protect revalidation API with secret token
What's Next?
You've mastered on-demand revalidation and cache tags! The final lesson in the Data Fetching section covers Handling Loading and Error States in Data Fetching—best practices for managing loading states, error boundaries, empty states, and providing excellent user experiences during data fetching.
You'll learn how to handle every scenario gracefully: slow loading, failed requests, empty results, and more. This completes your data fetching mastery!
🎯 Tag Everything
When in doubt, add cache tags to your fetch requests. They don't hurt performance and give you the flexibility to revalidate precisely when needed. Better to have tags you don't use than to need a tag you didn't add!