Next.js Cache Revalidation: On-Demand ISR, Cache Tags, Webhook Triggers, and revalidatePath
Master Next.js cache revalidation. Covers revalidatePath and revalidateTag for on-demand ISR, cache tag strategies for granular invalidation, webhook-triggered revalidation from CMS, stale-while-revalidate behavior, and the unstable_cache API.
Next.js App Router caching has four layers: Request Memoization (within a render), Data Cache (fetch responses across renders), Full Route Cache (rendered HTML), and Router Cache (client-side). Most revalidation mistakes come from not knowing which layer you're invalidating โ and which one is actually serving stale content.
This guide covers the practical revalidation patterns you'll use daily.
The Four Cache Layers (Quick Reference)
| Layer | What It Stores | Invalidated By |
|---|---|---|
| Request Memoization | fetch() results within one render | Automatic (per request) |
| Data Cache | fetch() results across renders | revalidateTag, revalidatePath, revalidate option |
| Full Route Cache | Rendered HTML + RSC payload | revalidatePath, time-based revalidate |
| Router Cache | Client-side prefetched pages | router.refresh(), navigation, 30s TTL |
revalidatePath: Invalidate by URL
// app/actions/posts.ts
"use server";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/prisma";
export async function publishPost(postId: string): Promise<void> {
await prisma.blogPost.update({
where: { id: postId },
data: { publishedAt: new Date(), status: "published" },
});
// Invalidate specific page: /blog/my-post-slug
revalidatePath(`/blog/${postId}`);
// Invalidate all blog pages (list pages that show this post)
revalidatePath("/blog", "page"); // "page" = only that exact path
revalidatePath("/blog", "layout"); // "layout" = path + all children
// Invalidate the home page if it shows recent posts
revalidatePath("/");
}
// After revalidatePath:
// 1. Next request to that path: serves stale HTML immediately
// 2. Background: regenerates the page
// 3. Subsequent requests: serve fresh HTML
// This is stale-while-revalidate behavior
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
revalidateTag: Granular Tag-Based Invalidation
Tags let you invalidate multiple pages that share data without knowing their URLs:
// lib/fetchers/posts.ts โ tag every fetch that uses post data
export async function getPost(slug: string) {
const res = await fetch(`${process.env.CMS_API_URL}/posts/${slug}`, {
next: {
tags: [`post:${slug}`, "posts"], // Multiple tags per fetch
revalidate: 3600, // Also revalidate every 1h
},
});
return res.json();
}
export async function getPosts(page: number) {
const res = await fetch(`${process.env.CMS_API_URL}/posts?page=${page}`, {
next: {
tags: ["posts"], // Tagged with "posts" โ invalidated when any post changes
revalidate: 600,
},
});
return res.json();
}
export async function getRelatedPosts(slug: string) {
const res = await fetch(`${process.env.CMS_API_URL}/posts/${slug}/related`, {
next: {
tags: [`post:${slug}`, "posts"],
},
});
return res.json();
}
// app/actions/cms.ts
"use server";
import { revalidateTag } from "next/cache";
// When a specific post is updated:
export async function invalidatePost(slug: string) {
revalidateTag(`post:${slug}`); // Only this post's pages
}
// When any post changes (e.g., new post published):
export async function invalidateAllPosts() {
revalidateTag("posts"); // All pages tagged with "posts"
}
unstable_cache: Cache Non-Fetch Data
For Prisma queries, Redis lookups, or any non-fetch async data that should be cached:
// lib/cache/workspace.ts
import { unstable_cache } from "next/cache";
import { prisma } from "@/lib/prisma";
// Cached Prisma query โ equivalent to fetch() with next.tags
export const getWorkspaceData = unstable_cache(
async (workspaceId: string) => {
return prisma.workspace.findUniqueOrThrow({
where: { id: workspaceId },
select: { id: true, name: true, plan: true, slug: true, logoUrl: true },
});
},
// Cache key prefix: ['workspace-data']
// Full key: ['workspace-data', workspaceId]
["workspace-data"],
{
tags: ["workspace"], // Tag for selective invalidation
revalidate: 300, // 5 minute TTL
}
);
// Invalidate workspace cache when settings change
// In Server Action:
import { revalidateTag } from "next/cache";
revalidateTag("workspace");
// Parameterized cache key pattern
export const getProjectStats = unstable_cache(
async (workspaceId: string, projectId: string) => {
const [tasks, completedTasks] = await Promise.all([
prisma.task.count({ where: { projectId } }),
prisma.task.count({ where: { projectId, status: "done" } }),
]);
return { tasks, completedTasks, completionPct: Math.round((completedTasks / tasks) * 100) };
},
["project-stats"],
{
tags: (workspaceId, projectId) => [`project:${projectId}`, "projects"],
revalidate: 60,
}
);
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
CMS Webhook: Trigger Revalidation Externally
// app/api/revalidate/route.ts โ secure webhook endpoint for CMS
import { NextRequest, NextResponse } from "next/server";
import { revalidateTag, revalidatePath } from "next/cache";
const WEBHOOK_SECRET = process.env.REVALIDATION_SECRET!;
interface CMSWebhookPayload {
event: "post.published" | "post.updated" | "post.deleted" | "settings.updated";
slug?: string;
model?: string;
}
export async function POST(req: NextRequest) {
// Verify webhook secret
const secret = req.headers.get("x-revalidation-secret");
if (secret !== WEBHOOK_SECRET) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body: CMSWebhookPayload = await req.json();
try {
switch (body.event) {
case "post.published":
case "post.updated":
if (body.slug) {
revalidateTag(`post:${body.slug}`); // This specific post
revalidateTag("posts"); // List pages
revalidatePath(`/blog/${body.slug}`);
}
break;
case "post.deleted":
if (body.slug) {
revalidateTag(`post:${body.slug}`);
revalidateTag("posts");
}
break;
case "settings.updated":
revalidateTag("settings");
revalidatePath("/", "layout"); // Invalidate root layout (affects all pages)
break;
default:
return NextResponse.json({ error: "Unknown event" }, { status: 400 });
}
return NextResponse.json({
revalidated: true,
event: body.event,
slug: body.slug,
timestamp: new Date().toISOString(),
});
} catch (err) {
console.error("[revalidate] Error:", err);
return NextResponse.json({ error: "Revalidation failed" }, { status: 500 });
}
}
Page-Level Cache Configuration
// app/blog/[slug]/page.tsx โ static page with on-demand revalidation
import { getPost } from "@/lib/fetchers/posts";
import { notFound } from "next/navigation";
// Generate static params at build time
export async function generateStaticParams() {
const posts = await getPost("__all__"); // Fetch all slugs from CMS
return posts.map((p: { slug: string }) => ({ slug: p.slug }));
}
// Force static generation + revalidate on demand only
export const revalidate = false; // Never automatically revalidate (use revalidateTag only)
// Or: export const revalidate = 3600; // Revalidate every 1 hour AND on demand
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) notFound();
return <PostContent post={post} />;
}
// Segment config options:
// export const dynamic = "force-static"; // Always static โ error if dynamic data used
// export const dynamic = "force-dynamic"; // Always server-rendered โ bypass all caches
// export const fetchCache = "only-cache"; // Error if any fetch() opts out of cache
Debugging: What's Cached?
# Next.js development mode shows cache status in terminal:
# GET /blog/my-post [HIT] โ served from cache
# GET /blog/my-post [MISS] โ fetched fresh, now cached
# GET /blog/my-post [SKIP] โ opted out of cache (dynamic)
# GET /blog/my-post [STALE] โ served stale, background revalidation triggered
# Disable caching globally for debugging:
# NEXT_DISABLE_CACHE=1 npm run dev
# Force specific fetch to never cache:
const data = await fetch(url, { cache: "no-store" });
# Force specific fetch to always use cache (even in dynamic routes):
const data = await fetch(url, { cache: "force-cache" });
Common Mistakes
// โ revalidatePath in a Server Component (no effect)
// revalidatePath only works in Server Actions and Route Handlers
export default async function Page() {
revalidatePath("/blog"); // Does nothing here
return <div>...</div>;
}
// โ
Correct: revalidatePath in a Server Action
"use server";
export async function updatePost() {
await db.update(...);
revalidatePath("/blog"); // โ
Works here
}
// โ Missing tag on fetch โ can't invalidate with revalidateTag
const data = await fetch(url); // No tags โ can only use revalidatePath
// โ
Always tag fetches that will need selective invalidation
const data = await fetch(url, { next: { tags: ["posts"] } });
// โ revalidateTag in a Server Action doesn't affect Router Cache
// User sees stale data from router cache for ~30s
// โ
Call router.refresh() on client after mutation:
"use client";
import { useRouter } from "next/navigation";
const router = useRouter();
await serverAction();
router.refresh(); // Busts Router Cache
Cost and Timeline Estimates
| Scope | Team | Timeline | Cost Range |
|---|---|---|---|
| Basic revalidatePath in Server Actions | 1 dev | Half a day | $150โ300 |
| Tag-based invalidation strategy | 1 dev | 1 day | $300โ600 |
| CMS webhook endpoint + secret verification | 1 dev | Half a day | $150โ300 |
| Full caching audit + optimization | 1โ2 devs | 2โ3 days | $800โ1,500 |
See Also
- Next.js App Router Caching Strategies
- Next.js Server Components Patterns
- Next.js Static Generation
- Next.js Performance Optimization
- AWS CloudFront Edge Caching
Working With Viprasol
Cache revalidation bugs are subtle โ stale content after mutations, cache misses on every request because tags aren't set, Router Cache serving old data after server-side invalidation. Our team audits your Next.js caching strategy: adds next.tags to every fetch that needs selective invalidation, moves revalidatePath/revalidateTag calls to Server Actions (not components), builds webhook endpoints for CMS-triggered revalidation, and pairs server invalidation with router.refresh() for Router Cache.
What we deliver:
- Four-layer cache mental model: Request Memoization / Data Cache / Full Route Cache / Router Cache
- Tag strategy:
post:${slug}for specific posts,postsfor list pages,workspace,settings unstable_cachewrapper for Prisma queries with tags and TTL- CMS webhook route:
x-revalidation-secretheader auth, event switch,revalidateTag+revalidatePath generateStaticParams+export const revalidatesegment configrouter.refresh()pattern for Router Cache bust after Server Action
Talk to our team about your Next.js caching architecture โ
Or explore our web development services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.