Next.js Interview Prep
Rendering Strategies

Incremental Static Regeneration (ISR)

Fresh Content Without Full Rebuilds

LinkedIn Hook

You deployed your e-commerce site with Static Site Generation. 500 product pages, blazing fast.

Then the product team updates a price. "When does the change go live?"

"After the next full rebuild... which takes 14 minutes."

Silence.

This is the exact problem Incremental Static Regeneration solves. ISR gives you the speed of static with the freshness of server-rendered. Pages regenerate in the background while users keep seeing the cached version — zero downtime, no full rebuild.

Yet in interviews, most candidates can't explain the difference between time-based and on-demand revalidation, don't know what stale-while-revalidate means, and confuse ISR with SSR.

In this lesson, I break down ISR completely: how it works under the hood, time-based vs on-demand revalidation, the fetch options that control it, and the mental model that makes it click.

If your interviewer asks "How do you keep static pages fresh?" — this is your answer.

Read the full lesson -> [link]

#NextJS #React #WebDevelopment #InterviewPrep #Frontend #CodingInterview #ISR #Rendering #100DaysOfCode


Incremental Static Regeneration (ISR) thumbnail


What You'll Learn

  • What ISR is and why it exists between SSG and SSR
  • How time-based revalidation works with the revalidate option
  • How on-demand revalidation works with revalidatePath and revalidateTag
  • How Next.js fetch options control caching and revalidation
  • What the stale-while-revalidate pattern means and why it matters
  • When to use ISR vs SSR vs SSG in real applications
  • How to answer ISR interview questions with precision

1. The Problem ISR Solves

The Bakery Analogy

Imagine you run a bakery. You have two options for serving bread:

Option A (SSG): Bake all your bread at 5 AM. Customers get bread instantly — no waiting. But if you run out of sourdough at noon, customers are out of luck until tomorrow's 5 AM bake.

Option B (SSR): Bake each loaf to order. Every customer gets perfectly fresh bread. But they wait 10 minutes for every single loaf, even during the lunch rush.

Option C (ISR): Bake all your bread at 5 AM (fast serving), but keep your oven running. When a customer asks for sourdough and it's been more than an hour since the last batch, you hand them the existing loaf immediately (still good!) and start baking a fresh batch in the background. The next customer gets the fresh one.

That's ISR. The customer never waits. The bread stays reasonably fresh. And you never shut down your bakery for a full rebake.

The Technical Problem

With pure SSG, your pages are generated at build time. A site with 10,000 product pages takes a long time to build. When a single product price changes, you have two bad options:

  1. Full rebuild — Rebuild all 10,000 pages for one price change. Wasteful and slow.
  2. Accept stale data — Wait for the next scheduled deploy. Unacceptable for time-sensitive content.

ISR eliminates this trade-off. Pages regenerate individually, in the background, without a full rebuild.


2. Time-Based Revalidation

Time-based revalidation is the simplest form of ISR. You tell Next.js: "This page is valid for N seconds. After that, regenerate it in the background."

How It Works — Step by Step

  1. Build time: Next.js generates the static HTML (just like SSG).
  2. User visits within revalidation window: They get the cached static page instantly.
  3. User visits AFTER the revalidation window expires: They still get the cached page (no waiting), but Next.js triggers a background regeneration.
  4. Next user after regeneration completes: They get the fresh page.

Page-Level Revalidation (App Router)

In the App Router, you export a revalidate constant from your page or layout file:

// app/products/[id]/page.tsx

// This page revalidates every 60 seconds
export const revalidate = 60;

async function getProduct(id: string) {
  const res = await fetch(`https://api.store.com/products/${id}`);
  return res.json();
}

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const product = await getProduct(params.id);

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span>${product.price}</span>
      <p>Last updated: {new Date().toISOString()}</p>
    </main>
  );
}

The export const revalidate = 60 tells Next.js: "Cache this page for 60 seconds. After 60 seconds, the next request triggers a background regeneration."

Fetch-Level Revalidation

You can also set revalidation at the individual fetch call level. This is more granular — different data sources on the same page can have different revalidation intervals:

// app/dashboard/page.tsx

export default async function Dashboard() {
  // Product data changes rarely — revalidate every hour
  const products = await fetch("https://api.store.com/products", {
    next: { revalidate: 3600 },
  });

  // Inventory changes frequently — revalidate every 30 seconds
  const inventory = await fetch("https://api.store.com/inventory", {
    next: { revalidate: 30 },
  });

  // User reviews change moderately — revalidate every 5 minutes
  const reviews = await fetch("https://api.store.com/reviews", {
    next: { revalidate: 300 },
  });

  const [productData, inventoryData, reviewData] = await Promise.all([
    products.json(),
    inventory.json(),
    reviews.json(),
  ]);

  return (
    <main>
      <ProductList products={productData} />
      <InventoryStatus inventory={inventoryData} />
      <ReviewSection reviews={reviewData} />
    </main>
  );
}

Important rule: When a page has multiple fetch calls with different revalidate values, the shortest interval wins for the page as a whole. If one fetch says 30 seconds and another says 3600 seconds, the page revalidates every 30 seconds.

The Timeline

Here's what happens with revalidate: 60:

Time 0s    → Build: page generated, cached
Time 1-59s → Requests: serve cached page (instant)
Time 60s   → Request: serve cached page + trigger background regeneration
Time 62s   → Background regeneration complete, cache updated
Time 63s   → Request: serve NEW cached page (instant)
Time 63-122s → Requests: serve new cached page
... cycle repeats

The critical insight: no user ever waits for regeneration. The "stale" user at time 60s gets the old page immediately. Only the next visitor sees the updated version.


3. On-Demand Revalidation

Time-based revalidation works well for content that changes at a predictable frequency. But what about:

  • A CMS author publishes a blog post and wants it live immediately?
  • An admin updates a product price and expects instant changes?
  • A user updates their profile and should see the update right away?

Waiting 60 seconds (or whatever your interval is) isn't good enough. You need on-demand revalidation — manually telling Next.js "this specific content is stale, regenerate it now."

revalidatePath

revalidatePath purges the cache for a specific URL path:

// app/api/revalidate/route.ts

import { revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";

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

  // Always protect revalidation endpoints with a secret
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ message: "Invalid secret" }, { status: 401 });
  }

  // Purge the cache for this specific path
  revalidatePath(path);

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

Now your CMS webhook can hit this endpoint when content changes:

# CMS webhook fires when a blog post is updated
curl -X POST https://yoursite.com/api/revalidate \
  -H "Content-Type: application/json" \
  -d '{"path": "/blog/isr-explained", "secret": "your-secret-token"}'

You can also use revalidatePath inside Server Actions for instant UI updates after mutations:

// app/products/[id]/actions.ts
"use server";

import { revalidatePath } from "next/cache";

export async function updateProductPrice(id: string, newPrice: number) {
  // Update the database
  await db.product.update({
    where: { id },
    data: { price: newPrice },
  });

  // Immediately invalidate the cached page
  revalidatePath(`/products/${id}`);
}

revalidateTag

revalidateTag is more powerful. Instead of invalidating by URL, you tag your fetch requests and invalidate by tag. One tag can cover multiple pages.

Step 1 — Tag your fetch calls:

// app/products/[id]/page.tsx

async function getProduct(id: string) {
  const res = await fetch(`https://api.store.com/products/${id}`, {
    next: {
      tags: [`product-${id}`, "products"],
    },
  });
  return res.json();
}

async function getRelatedProducts(category: string) {
  const res = await fetch(`https://api.store.com/products?category=${category}`, {
    next: {
      tags: ["products", `category-${category}`],
    },
  });
  return res.json();
}

Step 2 — Invalidate by tag:

// app/admin/actions.ts
"use server";

import { revalidateTag } from "next/cache";

export async function updateProduct(id: string, data: ProductData) {
  await db.product.update({ where: { id }, data });

  // Invalidate this specific product page
  revalidateTag(`product-${id}`);
}

export async function bulkUpdatePrices(category: string) {
  await db.product.updateMany({
    where: { category },
    data: { discount: 0.1 },
  });

  // Invalidate ALL pages that use products in this category
  revalidateTag(`category-${category}`);
}

export async function clearAllProductCaches() {
  // Nuclear option: invalidate every page that fetches any product
  revalidateTag("products");
}

The beauty of tags is cascading invalidation. When you update a product, you don't need to know every page that displays it. You just invalidate the tag, and every page that used that tag regenerates on the next request.

revalidatePath vs revalidateTag

FeaturerevalidatePathrevalidateTag
GranularityURL-basedData-based
ScopeSingle pageMultiple pages sharing the tag
Use caseKnown URL changedData changed, affects unknown pages
ExampleBlog post editedProduct category repriced
PrecisionInvalidates entire page cacheInvalidates specific cached fetches

4. Fetch Options — The Control Panel

In Next.js App Router, the fetch function is extended with a next option that controls caching behavior. Understanding these options is essential for interviews:

// DEFAULT: Cached indefinitely (equivalent to SSG behavior)
// Data is fetched once and reused for all requests
fetch("https://api.example.com/data");

// TIME-BASED REVALIDATION: Cache for N seconds (ISR behavior)
// After N seconds, next request triggers background refresh
fetch("https://api.example.com/data", {
  next: { revalidate: 60 },
});

// NO CACHE: Fetch fresh on every request (SSR behavior)
// Never cache, always hit the API
fetch("https://api.example.com/data", {
  cache: "no-store",
});

// TAGGED: Cache with tags for on-demand invalidation
// Stays cached until you call revalidateTag()
fetch("https://api.example.com/data", {
  next: { tags: ["my-data"] },
});

// COMBINED: Tags + time-based revalidation
// Revalidates on timer OR when tag is manually invalidated
fetch("https://api.example.com/data", {
  next: {
    revalidate: 3600,
    tags: ["my-data"],
  },
});

The hierarchy of caching behavior:

  1. cache: "no-store" overrides everything — always fresh, never cached.
  2. revalidate: 0 is equivalent to cache: "no-store".
  3. revalidate: N caches for N seconds, then revalidates in the background.
  4. No option specified — cached indefinitely (static behavior).

5. Stale-While-Revalidate — The Mental Model

ISR implements the stale-while-revalidate pattern, borrowed from HTTP caching (Cache-Control: stale-while-revalidate). This is the mental model interviewers want you to explain:

The Three States of a Cached Page

┌─────────────────────────────────────────────────────────────┐
│              STALE-WHILE-REVALIDATE TIMELINE                │
│                                                             │
│  BUILD          revalidate         regeneration             │
│    │            window expires     complete                  │
│    ▼                 ▼                  ▼                    │
│ ───●────── FRESH ────●──── STALE ──────●──── FRESH ────►   │
│    │                 │                  │                    │
│    │  Serve cached   │  Serve cached    │  Serve NEW        │
│    │  page (fast)    │  page (fast)     │  cached page      │
│    │                 │  + regenerate    │  (fast)            │
│    │                 │  in background   │                    │
│                                                             │
│  STATE 1: FRESH     STATE 2: STALE     STATE 3: FRESH      │
│  (within window)    (serve + regen)    (updated cache)      │
│                                                             │
│  User experience: ALWAYS instant. Never a loading spinner.  │
└─────────────────────────────────────────────────────────────┘

Why this matters:

  • SSR = user waits for every request. Fresh, but slow.
  • SSG = user never waits. Fast, but stale until next build.
  • ISR = user never waits AND content stays fresh. Best of both worlds.

The trade-off is eventual consistency. The user who triggers the revalidation sees stale data. The next user sees fresh data. For most content (products, blog posts, profiles), a few seconds of staleness is perfectly acceptable.

When Stale-While-Revalidate Breaks Down

This pattern does NOT work for:

  • Real-time data (stock prices, live scores) — use SSR or client-side fetching
  • User-specific data (shopping cart, notifications) — use client components
  • Security-sensitive data (permissions that just changed) — use SSR with no cache

Incremental Static Regeneration (ISR) visual 1


Common Mistakes

  1. Setting revalidate: 0 and expecting caching. A revalidation interval of 0 is equivalent to cache: "no-store" — it disables caching entirely and makes the page SSR. Use revalidate: 1 if you want the shortest possible ISR window.

  2. Not protecting revalidation API routes. If you expose an endpoint that calls revalidatePath or revalidateTag without authentication, anyone can purge your cache. Always require a secret token.

  3. Confusing page-level and fetch-level revalidation. export const revalidate = 60 applies to the entire page. fetch(..., { next: { revalidate: 60 } }) applies to a single data request. If both are set, the shortest interval wins.

  4. Using ISR for user-specific content. ISR caches at the page level — every user sees the same cached page. If the page shows personalized data (user name, cart items), ISR will serve one user's data to another. Use client-side fetching for personalized content.

  5. Expecting instant updates with time-based revalidation. If revalidate: 60, the worst case is a user seeing data that is 60 seconds old. If you need instant updates, use on-demand revalidation with revalidateTag or revalidatePath.

  6. Forgetting that revalidation requires a running server. ISR only works when your Next.js app runs on a Node.js server (or serverless functions). If you use output: 'export' for a fully static site, ISR is not available.


Interview Questions

Q1: What is ISR and how does it differ from SSG and SSR?

Answer: ISR (Incremental Static Regeneration) is a rendering strategy that combines the performance of SSG with the freshness of SSR. Like SSG, pages are pre-rendered as static HTML and served from cache. Unlike SSG, pages can regenerate in the background after a specified time interval without requiring a full site rebuild. Unlike SSR, users never wait for the page to render on each request — they always receive the cached version immediately while regeneration happens in the background.

Q2: Explain the stale-while-revalidate pattern in the context of ISR.

Answer: In ISR, a cached page goes through three states. First, it's FRESH — within the revalidation window, every user gets the cached page instantly. Second, it becomes STALE — after the revalidation window expires, the next request still serves the cached page immediately (user sees no delay), but triggers a background regeneration on the server. Third, once regeneration completes, the cache is updated and subsequent requests receive the new page. The key insight is that no user ever waits for regeneration — the "stale" user gets instant response with slightly old data, which is acceptable for most content types.

Q3: What is the difference between revalidatePath and revalidateTag?

Answer: revalidatePath invalidates the cache for a specific URL path — you need to know the exact URL. revalidateTag invalidates all cached fetch requests that were tagged with a specific string. Tags are more flexible because one tag can span multiple pages. For example, if 50 pages display products from the "electronics" category and you tag all those fetches with category-electronics, a single revalidateTag("category-electronics") call invalidates all 50 pages. With revalidatePath, you'd need to know and invalidate all 50 URLs individually.

Q4: Can you use ISR with dynamic routes? How?

Answer: Yes. You combine generateStaticParams with a revalidate export. generateStaticParams tells Next.js which dynamic paths to pre-render at build time, and revalidate controls how often those pages regenerate. Pages for paths not included in generateStaticParams can also be generated on-demand — the first request triggers SSR, the result is cached, and subsequent requests within the revalidation window get the cached version.

// app/blog/[slug]/page.tsx
export const revalidate = 300; // 5 minutes

export async function generateStaticParams() {
  const posts = await fetch("https://api.blog.com/posts").then(r => r.json());
  // Pre-render the 100 most popular posts at build time
  return posts.slice(0, 100).map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.blog.com/posts/${params.slug}`, {
    next: { tags: [`post-${params.slug}`] },
  }).then(r => r.json());

  return <article><h1>{post.title}</h1><div>{post.content}</div></article>;
}

Q5: When should you NOT use ISR?

Answer: ISR should not be used for: (1) Real-time data like stock prices or live chat — use SSR or client-side fetching with WebSockets. (2) User-specific content like dashboards or shopping carts — ISR caches are shared across all users, so personalized data would leak between users. (3) Authentication-gated pages where stale permission data could be a security risk. (4) Static export deployments — ISR requires a running server to perform background regeneration.


Cheat Sheet

┌─────────────────────────────────────────────────────────────┐
│                    ISR CHEAT SHEET                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  TIME-BASED REVALIDATION                                    │
│  export const revalidate = 60        (page-level, 60s)      │
│  fetch(url, { next: { revalidate: 60 } })  (fetch-level)   │
│  Shortest interval wins when multiple are set               │
│                                                             │
│  ON-DEMAND REVALIDATION                                     │
│  revalidatePath("/blog/my-post")     (purge specific URL)   │
│  revalidateTag("products")           (purge by tag)         │
│  Use in Server Actions or Route Handlers                    │
│                                                             │
│  FETCH TAGS                                                 │
│  fetch(url, { next: { tags: ["products", "featured"] } })  │
│  One fetch can have multiple tags                           │
│  One tag can span multiple fetches/pages                    │
│                                                             │
│  CACHING BEHAVIOR                                           │
│  ┌────────────────────────┬──────────────────────────┐      │
│  │ Option                 │ Behavior                 │      │
│  ├────────────────────────┼──────────────────────────┤      │
│  │ (default, no option)   │ Cached forever (SSG)     │      │
│  │ revalidate: N          │ Cache N seconds (ISR)    │      │
│  │ revalidate: 0          │ No cache (SSR)           │      │
│  │ cache: "no-store"      │ No cache (SSR)           │      │
│  │ tags: [...]            │ Cached until invalidated │      │
│  └────────────────────────┴──────────────────────────┘      │
│                                                             │
│  STALE-WHILE-REVALIDATE FLOW                                │
│  Build → FRESH → expires → STALE (serve + regen) → FRESH   │
│  User NEVER waits. Background regeneration is invisible.    │
│                                                             │
│  WHEN TO USE ISR                                            │
│  Blog posts, product pages, marketing pages, docs           │
│  Content that changes but not in real-time                  │
│                                                             │
│  WHEN NOT TO USE ISR                                        │
│  Real-time data, user-specific content, static exports      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Previous: Lesson 2.2 — Static Site Generation (SSG) -> Next: Lesson 2.4 — Client-Side Rendering (CSR) ->


This is Lesson 2.3 of the Next.js Interview Prep Course — 8 chapters, 33 lessons.

On this page