VercelVercel
Menu
 

Getting started with ISR

Last updated April 9, 2025

This guide helps you set up Incremental Static Regeneration (ISR) with your Vercel project. With ISR, you can regenerate pages without rebuilding and redeploying your site. When a page with ISR enabled regenerates, Vercel fetches the most recent data and updates the cache. There are two ways to trigger regeneration:

  • Time-based revalidation: Regeneration that recurs automatically at a set interval
  • On-demand revalidation: Regeneration that you trigger explicitly through an API call

You also control when pages are first cached:

  • Pre-render at build time: Generate pages during the build so the first visitor gets an instant cache hit. This increases build time but avoids slow first requests.
  • Generate on first request: Skip the build step and let the first visitor trigger generation at runtime. This keeps builds fast but means the first request for each page is slower (a cache miss).

A common pattern is to pre-render popular pages at build time and let the rest generate on demand.

  • A project deployed on Vercel
  • A supported framework: Next.js, SvelteKit, Nuxt, Astro, Gatsby, or a custom solution using the Build Output API
FrameworkISR supportOn-demand revalidation
Next.js (App Router)revalidate route segment configrevalidatePath / revalidateTag
Next.js (Pages Router)revalidate in getStaticPropsres.revalidate API route
SvelteKitconfig.isr exportx-prerender-revalidate header
NuxtrouteRules with isr optionx-prerender-revalidate header
AstroServer output with ISR configFramework-specific
GatsbyDeferred Static Generation (DSG)Framework-specific

Time-based revalidation purges the cache for an ISR route automatically at a set interval. When the interval elapses and a visitor requests the page, Vercel serves the stale version and regenerates the page in the background.

When using Next.js with the App Router, you can enable ISR by using the revalidate route segment config for a layout or page.

apps/example/page.tsx
export const revalidate = 10; // seconds

The following example renders a list of blog posts from a demo API, revalidating every 10 seconds:

pages/blog-posts/index.tsx
export async function getStaticProps() {
  const res = await fetch('https://api.vercel.app/blog');
  const posts = await res.json();
 
  return {
    props: {
      posts,
    },
    revalidate: 10,
  };
}
 
interface Post {
  title: string;
  id: number;
}
 
export default function BlogPosts({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
app/blog-posts/page.tsx
export const revalidate = 10; // seconds
 
interface Post {
  title: string;
  id: number;
}
 
export default async function Page() {
  const res = await fetch('https://api.vercel.app/blog');
  const posts = (await res.json()) as Post[];
  return (
    <ul>
      {posts.map((post: Post) => {
        return <li key={post.id}>{post.title}</li>;
      })}
    </ul>
  );
}

To test this code, run the appropriate dev command for your framework and navigate to the /blog-posts/ route.

You should see a bulleted list of blog posts.

On-demand revalidation lets you purge the cache for an ISR route at any time, without waiting for a time interval to elapse. This is useful when your content changes based on external events, such as a CMS publish or a webhook.

Tag-based revalidation is the recommended approach for granular control. Instead of revalidating entire paths, you tag cached content and invalidate specific tags when the underlying data changes.

To revalidate a page on demand with Next.js:

  1. Create an Environment Variable which will store a revalidation secret
  2. Create an API Route that checks for the secret, then triggers revalidation

The following example demonstrates an API route that triggers revalidation if the query paramater ?secret matches a secret Environment Variable:

pages/api/revalidate.js
export default async function handler(request, response) {
  // Check for secret to confirm this is a valid request
  if (request.query.secret !== process.env.MY_SECRET_TOKEN) {
    return response.status(401).json({ message: 'Invalid token' });
  }
 
  try {
    // This should be the actual path, not a rewritten path
    // e.g. for "/blog-posts/[slug]" this should be "/blog-posts/1"
    await response.revalidate('/blog-posts');
    return response.json({ revalidated: true });
  } catch (err) {
    // If there was an error, Next.js will continue
    // to show the last successfully generated page
    return response.status(500).send('Error revalidating');
  }
}
pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';
 
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  // Check for secret to confirm this is a valid request
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' });
  }
 
  try {
    // This should be the actual path, not a rewritten path
    // e.g. for "/blog-posts/[slug]" this should be "/blog-posts/1"
    await res.revalidate('/blog-posts');
    return res.json({ revalidated: true });
  } catch (err) {
    // If there was an error, Next.js will continue
    // to show the last successfully generated page
    return res.status(500).send('Error revalidating');
  }
}
app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  if (searchParams.get('secret') !== process.env.MY_SECRET_TOKEN) {
    return new Response('Invalid credentials', {
      status: 401,
    });
  }
 
  revalidatePath('/blog-posts');
 
  return Response.json({
    revalidated: true,
    now: Date.now(),
  });
}
app/api/revalidate/route.js
import { revalidatePath } from 'next/cache';
 
export async function GET(request) {
  const { searchParams } = new URL(request.url);
  if (searchParams.get('secret') !== process.env.MY_SECRET_TOKEN) {
    return new Response('Invalid credentials', {
      status: 401,
    });
  }
 
  revalidatePath('/blog-posts');
 
  return Response.json({
    revalidated: true,
    now: Date.now(),
  });
}

See the time-based revalidation section above for a full ISR example.


Was this helpful?

supported.