All posts
nextjsisr

Incremental Static Regeneration: A Practical Guide for Full-Stack Developers

A practical guide to Incremental Static Regeneration — setup, core concepts, common mistakes, and production tips for full-stack developers.

SR

Suhail Roushan

August 6, 2026

·
7 min read
·
0 views

Static sites are fast, but they go stale the moment your data changes — Incremental Static Regeneration gives you the best of both worlds without rebuilding the whole site.

Incremental Static Regeneration (ISR) lets you update static pages after deployment, on-demand or on a schedule, without sacrificing the performance benefits of a static site. It's the middle ground between full SSG (build everything upfront) and SSR (render on every request). If you're a full-stack developer building content-heavy apps with Next.js, you've likely hit the wall where static generation feels too rigid and server rendering feels too slow. ISR solves that by letting you invalidate and regenerate individual pages at runtime.

Why Incremental Static Regeneration Matters (and When to Skip It)

Here's my take: most teams over-engineer their rendering strategy. If your content changes less than once a minute and you don't need per-user personalization, ISR is almost always the right call. It gives you CDN-level performance with database-level freshness.

But skip it if you're building a dashboard with real-time data or a social feed where every user sees different content. ISR is for public, URL-addressable pages — not authenticated, personalized views. Also skip it if your content changes more frequently than your traffic can trigger revalidation, because you'll end up with stale pages and wasted revalidation calls.

Getting Started with Incremental Static Regeneration

The simplest ISR setup is a getStaticProps function with a revalidate property. Here's a minimal, runnable example:

// pages/products/[id].tsx
import { GetStaticProps, GetStaticPaths } from 'next';

interface Product {
  id: string;
  name: string;
  price: number;
}

export const getStaticPaths: GetStaticPaths = async () => {
  // Pre-render only the top 10 products at build time
  const products = await fetch('https://api.example.com/products?limit=10').then(r => r.json());
  
  return {
    paths: products.map((p: Product) => ({ params: { id: p.id } })),
    fallback: 'blocking', // Generate on-demand for unknown paths
  };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const product = await fetch(`https://api.example.com/products/${params?.id}`).then(r => r.json());
  
  return {
    props: { product },
    revalidate: 60, // Regenerate at most once every 60 seconds
  };
};

export default function ProductPage({ product }: { product: Product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
    </div>
  );
}

That's it. Deploy this, and Next.js will serve the static page from the CDN. After 60 seconds, the first request triggers a background regeneration, and the new version gets swapped in. Users never wait — they get the old version while the new one builds.

Core Incremental Static Regeneration Concepts Every Developer Should Know

1. On-demand revalidation

The revalidate interval is a minimum, not a guarantee. If you need instant updates (e.g., a price change after a sale), use res.revalidate() from an API route:

// pages/api/revalidate.ts
import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.query.secret !== process.env.REVALIDATE_SECRET) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  try {
    await res.revalidate(`/products/${req.query.id}`);
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).json({ message: 'Revalidation failed' });
  }
}

Call this endpoint from your CMS webhook after a content update. This is the pattern I use in production — it's instant, and it doesn't wait for the time window.

2. Fallback modes

The fallback option in getStaticPaths controls what happens for unvisited paths:

export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [],
    fallback: true, // or 'blocking' or false
  };
};
  • fallback: false — 404 for any path not generated at build time.
  • fallback: true — serve a loading state, then fetch the page client-side.
  • fallback: 'blocking' — the server waits for the page to generate and serves it directly. I prefer this for SEO-sensitive pages because there's no client-side flicker.

3. Stale-while-revalidate behavior

ISR uses a stale-while-revalidate pattern under the hood. The first request after the revalidate window gets the stale page, triggers a background rebuild, and subsequent requests get the fresh version. This means your time-to-first-byte (TTFB) stays constant — no request ever waits for a rebuild.

// Visualizing the lifecycle
// t=0    → Build → Serve static page
// t=30   → Request → Serve stale (t=0) → Trigger rebuild
// t=31   → Rebuild complete → New page cached
// t=32   → Request → Serve fresh (t=31)

Common Incremental Static Regeneration Mistakes and How to Fix Them

Mistake 1: Revalidating too aggressively. Setting revalidate: 5 on a high-traffic site means constant background rebuilds. Fix: set it to 60 or 300 seconds, and use on-demand revalidation for urgent updates.

Mistake 2: Ignoring the revalidate flag in getStaticProps. If you return revalidate: false or omit it, the page is built once and never updated. I've seen teams wonder why their content is stale for days — check that the property is present.

Mistake 3: Forgetting about the CDN cache. ISR works at the Next.js server level, but if you're behind a CDN with aggressive caching, you might serve stale content anyway. Fix: set Cache-Control: s-maxage=60, stale-while-revalidate=59 on your response headers to align CDN caching with your revalidation window.

When Should You Use Incremental Static Regeneration?

Use ISR when you have public, URL-addressable pages with content that changes periodically — think blog posts, product listings, marketing pages, and documentation. It's ideal when you want the performance of static generation but can't afford to rebuild the entire site every time one page changes.

Skip ISR if you need real-time data (use SSR or client-side fetching), if your pages are personalized per user (use SSR), or if your content updates more frequently than your traffic can trigger revalidation (you'll get a build backlog).

The sweet spot is content that changes every few minutes to a few hours — that's where ISR shines and where you'll see the biggest infrastructure cost savings compared to SSR.

Incremental Static Regeneration in Production

Tip 1: Separate build and revalidation secrets. Use a different environment variable for the revalidation endpoint than your CMS credentials. I've seen leaks where the CMS token was exposed in the webhook URL.

Tip 2: Monitor revalidation failures. Add logging to your revalidate API route. If the upstream fetch fails during regeneration, the page stays stale indefinitely and you won't know unless you're watching logs.

Tip 3: Pre-render your top pages at build time. Don't rely entirely on on-demand generation for your most-trafficked pages. Generate the top 10-20% of your catalog at build time to avoid the cold-start penalty for popular content.

Here's a production-grade revalidation setup with error handling:

// pages/api/revalidate.ts
import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }

  const secret = req.headers['x-revalidate-secret'];
  if (secret !== process.env.REVALIDATE_SECRET) {
    return res.status(401).json({ message: 'Invalid secret' });
  }

  const paths = Array.isArray(req.body.paths) ? req.body.paths : [req.body.path];
  
  try {
    await Promise.all(paths.map((path: string) => res.revalidate(path)));
    console.log(`Revalidated: ${paths.join(', ')}`);
    return res.json({ revalidated: true, paths });
  } catch (err) {
    console.error('Revalidation failed:', err);
    return res.status(500).json({ message: 'Revalidation failed' });
  }
}

Start with a single product page, set revalidate: 60, and wire up one webhook from your CMS. Measure the TTFB before and after — you'll see the difference immediately. That's the fastest way to validate whether ISR fits your stack.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch