One of the biggest challenges I’ve faced when scaling static sites is the ‘stale data’ problem. Standard Incremental Static Regeneration (ISR) is great, but relying on a 60-second or 1-hour timer means your users are often looking at outdated content. When you’re managing a high-traffic blog or an e-commerce store, waiting for a timer is unacceptable.

That’s where on-demand ISR comes in. Instead of telling Next.js to check for updates every X seconds, you tell it exactly when to update. In this tutorial, I’ll show you exactly how to implement on-demand ISR in Next.js so your content updates the millisecond you hit ‘Save’ in your CMS.

Prerequisites

Before we dive into the code, make sure you have the following set up:

Step 1: Configure Your Static Page

First, we need a page that is statically generated. In the App Router, pages are static by default unless you use dynamic functions. I usually set up my product or blog pages like this:

// app/posts/[slug]/page.tsx
import { getPostBySlug } from '@/lib/api';

export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);

  return (
    <article>
      
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Step 2: Create the Revalidation API Route

To trigger the update, we need a secure endpoint that our CMS can call via a webhook. This is the core of how to implement on-demand ISR in Next.js. We’ll use a Route Handler in the App Router.

// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath, revalidateTag } from 'next/cache';

export async function POST(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get('secret');

  // 1. Security Check: Prevent unauthorized revalidation
  if (secret !== process.env.MY_SECRET_TOKEN) {
    return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
  }

  try {
    const body = await request.json();
    const slug = body.slug; // Assuming CMS sends the slug of the changed post

    // 2. Revalidate the specific path
    revalidatePath(`/posts/${slug}`);
    
    // Optional: Revalidate a tag if you're using fetch tags
    // revalidateTag('posts');

    return NextResponse.json({ revalidated: true, now: Date.now() });
  } catch (err) {
    return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
  }
}
Technical flowchart showing the on-demand ISR request flow from CMS to Next.js API route
Technical flowchart showing the on-demand ISR request flow from CMS to Next.js API route

As shown in the image below, the logic flow involves a secure handshake between your CMS and this endpoint to ensure random users can’t trigger expensive rebuilds of your site.

Step 3: Setting Up the CMS Webhook

Now that the endpoint exists, you need to tell your CMS to call it. In my experience, this is where most developers get stuck. Here is the general workflow:

  1. Go to your CMS settings → Webhooks.
  2. Set the URL to https://yourdomain.com/api/revalidate?secret=your_token_here.
  3. Select the trigger event (e.g., “Post Published” or “Post Updated”).
  4. Ensure the payload is sent as JSON and contains the slug of the post.

If you’re integrating this with a headless WordPress with Next.js guide, you might use a plugin like WP Webhooks to handle this trigger.

Pro Tips for Production

1. Use revalidateTag for Bulk Updates

If you update a category name that affects 100 posts, calling revalidatePath 100 times is inefficient. Instead, use revalidateTag. When fetching data, add a tag to your request:

fetch('https://api.example.com/posts', { next: { tags: ['posts'] } })

Then, in your API route, simply call revalidateTag('posts') to purge the entire cache for those requests.

2. Handle Race Conditions

I’ve found that some CMSs send multiple webhooks for a single save. To prevent unnecessary load, consider adding a small debounce or a check to see if the content has actually changed before calling the revalidation function.

Troubleshooting Common Issues

Issue Likely Cause Solution
401 Unauthorized Token mismatch Check your .env and the webhook URL query string.
Page doesn’t update Caching layer (CDN) Check if you have a CDN like Cloudflare in front of Vercel that needs its own purge.
500 Internal Error Payload mismatch Log request.json() to ensure the CMS is sending the slug as expected.

What’s Next?

Now that you’ve mastered on-demand ISR, you might be wondering if you’re using the right framework for your specific scale. If you find that your site is purely static with very rare updates, you might want to explore Astro vs Next.js for static sites to see if a different architecture would better serve your needs.

Ready to optimize your site? Start by implementing the revalidation route today and stop relying on timers!