Next.js Incremental Static Regeneration (ISR)

Incremental Static Regeneration gives you the speed of a static site with the freshness of server-side rendering. Pages are pre-built like SSG, but they automatically refresh in the background on a schedule you control. You get fast delivery and up-to-date content — without rebuilding the entire site.

The Hotel Room Analogy

Think of a hotel that cleans rooms on a schedule. The room is always ready (pre-built static page) when a guest arrives. Every night, housekeeping comes through and refreshes each room (background regeneration). Guests never wait — they always get a clean, ready room. And the room gets updated regularly without the hotel shutting down.

Pure SSG:
  Build → Generate all pages → Serve forever (stale until rebuild)

Pure SSR:
  Each request → Generate page fresh → Serve (always current, slower)

ISR:
  Build → Generate pages → Serve (fast)
  Background → Regenerate pages after revalidate time → Serve updated version

Setting Up ISR With revalidate

Add next: { revalidate: seconds } to your fetch call to enable ISR. The number is how many seconds Next.js waits before regenerating the page after the next visit.

// app/blog/page.js
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 3600 }, // Regenerate at most once per hour
  });
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

You can also set revalidate as a route segment export instead of inside fetch:

// app/blog/page.js
export const revalidate = 3600; // 1 hour — applies to the entire route

async function getPosts() {
  const res = await fetch("https://api.example.com/posts");
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();
  return ( /* ... */ );
}

How ISR Works Step by Step

Step 1: Build time
  Next.js builds /blog and saves the HTML file.

Step 2: First visitor (within revalidate window)
  Visitor arrives → gets cached HTML instantly → fast ✓
  No regeneration triggered yet.

Step 3: Visitor arrives after revalidate time expires
  Visitor arrives → gets OLD cached HTML (still fast) ✓
  In background → Next.js regenerates the page with fresh data.

Step 4: Next visitor after regeneration finishes
  Gets the NEW HTML ✓

Key insight: The first visitor after expiry gets the stale page.
            The NEXT visitor gets the fresh page.
            Nobody waits.

Diagram: ISR Timeline

revalidate = 60 seconds

Time 0:00  → Page built, cached as v1
Time 0:05  → Visitor A → gets v1 (fast)
Time 0:30  → Visitor B → gets v1 (fast)
Time 0:55  → Visitor C → gets v1 (fast)

[60 seconds expire]

Time 1:05  → Visitor D → gets v1 (still fast!)
              → Triggers background regen with fresh data
              → Next.js fetches new data, builds v2

Time 1:08  → Background regen finishes, v2 saved

Time 1:15  → Visitor E → gets v2 (fresh data!) ✓

ISR With Dynamic Routes

Dynamic pages like blog posts also support ISR. Combine generateStaticParams with revalidate to pre-build known pages and keep them fresh.

// app/blog/[slug]/page.js
export const revalidate = 1800; // Regenerate after 30 minutes

export async function generateStaticParams() {
  const posts = await fetch("https://api.example.com/posts").then((r) => r.json());
  return posts.map((post) => ({ slug: post.slug }));
}

async function getPost(slug) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 1800 },
  });
  return res.json();
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

On-Demand Revalidation

Sometimes you do not want to wait for a timer. When a new blog post publishes, you want the blog list page updated immediately. On-demand revalidation lets you trigger a page refresh instantly from an API route.

Method 1: Revalidate a Path

// app/api/revalidate/route.js
import { revalidatePath } from "next/cache";
import { NextResponse } from "next/server";

export async function POST(request) {
  const { path, secret } = await request.json();

  // Protect with a secret token
  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  revalidatePath(path);  // e.g., "/blog" or "/blog/my-post"

  return NextResponse.json({ revalidated: true, path });
}

Your CMS calls this API endpoint when content changes. Next.js immediately marks that path as stale and regenerates it on the next request.

Method 2: Revalidate a Cache Tag

// Tag the fetch with a label
async function getPost(slug) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: {
      revalidate: 3600,
      tags: ["posts", `post-${slug}`],  // Give this data a tag
    },
  });
  return res.json();
}

// Later, invalidate all pages that use tagged data
// app/api/revalidate/route.js
import { revalidateTag } from "next/cache";

export async function POST(request) {
  const { tag } = await request.json();
  revalidateTag(tag);  // e.g., "posts" invalidates all pages tagged "posts"
  return Response.json({ revalidated: true });
}

Diagram: On-Demand Revalidation Flow

Content editor publishes new post in CMS
              │
              ▼
CMS triggers webhook → POST /api/revalidate { path: "/blog" }
              │
              ▼
Next.js marks /blog cache as stale
              │
              ▼
Next visitor arrives at /blog
              │
              ▼
Next.js regenerates /blog with fresh data (visible to visitor)
              │
              ▼
All future visitors get updated page ✓

Choosing a revalidate Interval

Content Type              Suggested revalidate
──────────────────────    ────────────────────
News articles             60–300 seconds
Blog posts                1800–3600 seconds (30–60 min)
Product pages             3600 seconds (1 hour)
Documentation             86400 seconds (1 day) or on-demand
Marketing pages           86400 seconds or on-demand
User-generated content    Use on-demand revalidation

false and 0 Values

export const revalidate = false;
// Cache forever — never auto-regenerate (same as pure SSG)

export const revalidate = 0;
// Never cache — always regenerate (same as SSR / force-dynamic)

ISR With Multiple Fetches

When a page has multiple fetch calls with different revalidate values, Next.js uses the lowest value as the effective revalidation time for the entire page.

// This page will revalidate every 60 seconds
// because 60 is lower than 3600
const posts = await fetch("/api/posts", { next: { revalidate: 3600 } });
const trending = await fetch("/api/trending", { next: { revalidate: 60 } });
//                                                      ↑ lowest wins

Fallback Behavior for New Pages

When dynamicParams = true (the default), a new page slug that was not in generateStaticParams gets server-rendered on the first request and then cached as a static ISR page for future visitors.

Timeline for a new blog post published after build:

Request 1 (first visitor):
  → Page not in cache
  → Server renders page fresh
  → Page saved to ISR cache
  → Visitor receives page

Request 2+ (all future visitors):
  → Page found in cache
  → Served instantly (static speed)
  → Regenerates in background after revalidate time

ISR vs Other Strategies Summary

Strategy    Speed      Freshness       Cost
────────    ─────      ─────────       ────
SSG         Fastest    Stale (rebuild) Low
ISR         Fast       Auto-refresh    Low
SSR         Moderate   Always fresh    Higher
CSR         Slow init  Real-time       Higher

Common ISR Mistakes

Mistake 1: Expecting immediate updates

// After setting revalidate: 3600
// You publish a new post
// Visit /blog — it still shows old posts

// Why: The revalidate timer hasn't expired yet
// Fix: Use on-demand revalidation with revalidatePath() or revalidateTag()

Mistake 2: Mixing revalidate with cache: "no-store"

// ❌ This cancels out ISR — the fetch opts out of caching entirely
const res = await fetch("/api/data", {
  cache: "no-store",
  next: { revalidate: 60 },  // Ignored because cache: "no-store" wins
});

Mistake 3: Using ISR for user-specific pages

// ❌ Wrong: ISR caches the same HTML for all users
// If page shows "Hello, Alice", ISR saves that HTML
// Bob visits → gets "Hello, Alice" from cache!

// ✓ Correct: Use SSR for user-specific pages
export const dynamic = "force-dynamic";

Summary

ISR combines the speed of static HTML with the freshness of periodic regeneration. Set a revalidate time on your fetch calls or as a route segment export. Use on-demand revalidation with revalidatePath or revalidateTag to refresh pages instantly when content changes. ISR is the sweet spot for most content-heavy sites — blogs, e-commerce, documentation, and marketing pages that update regularly but not every second.

Leave a Comment