Back to Blog

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.

Viprasol Tech Team
May 27, 2027
12 min read

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)

LayerWhat It StoresInvalidated By
Request Memoizationfetch() results within one renderAutomatic (per request)
Data Cachefetch() results across rendersrevalidateTag, revalidatePath, revalidate option
Full Route CacheRendered HTML + RSC payloadrevalidatePath, time-based revalidate
Router CacheClient-side prefetched pagesrouter.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

ScopeTeamTimelineCost Range
Basic revalidatePath in Server Actions1 devHalf a day$150โ€“300
Tag-based invalidation strategy1 dev1 day$300โ€“600
CMS webhook endpoint + secret verification1 devHalf a day$150โ€“300
Full caching audit + optimization1โ€“2 devs2โ€“3 days$800โ€“1,500

See Also


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, posts for list pages, workspace, settings
  • unstable_cache wrapper for Prisma queries with tags and TTL
  • CMS webhook route: x-revalidation-secret header auth, event switch, revalidateTag+revalidatePath
  • generateStaticParams + export const revalidate segment config
  • router.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.

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol ยท Web Development

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.