Static generation and incremental regeneration
Render pages to HTML once, ahead of time, and serve the same files from a CDN to everyone; incremental regeneration then refreshes single pages after launch, so you get static speed without rebuilding the whole site for every edit.
The same page, rendered a million times
Picture a recipe site with 40,000 recipes. It uses server-side rendering (SSR): every time someone opens a recipe, a Node.js server fetches the recipe from the database, renders the React tree to HTML, and sends it back. It takes about 180 ms of server time per request, and the page looks good on a slow phone because the HTML arrives complete.
Now look at what the server actually does all day. The lasagna recipe was last edited eight months ago. Yet the server renders it again for every visitor: 30,000 times a day, each time producing exactly the same bytes. On a busy evening, a TV mention sends 2,000 requests per second to that one page, the render servers queue up, and the time to first byte (TTFB, the time until the first byte of the response arrives) climbs from 200 ms to 3 seconds.
Static site generation (SSG) removes the repeated work. You render each page once, when you build the site, and save the result as a plain .html file. A content delivery network (CDN, a network of cache servers spread around the world) then hands that file to everyone. No render server runs on the request path at all.
What SSR pays for, and when you don't need it
The SSR lesson showed that rendering on the server buys you a fast first paint and crawlable HTML. It also showed the price: a server that must be up, fast, and close to the user on every single request. That price makes sense when the HTML is different per request: a logged-in dashboard, a search results page, a cart.
Many pages are not like that. A blog post, a docs page, a marketing page, or a product description is the same for every visitor. For those pages, rendering per request is like a bakery that bakes a fresh copy of the same loaf for each customer while a shelf of identical loaves could have been baked at dawn.
- SSR: render on every request. HTML is always fresh, but each request costs server time and the server is on the critical path.
- SSG: render once at build time. Each request costs a file read from a nearby cache, but the HTML is only as fresh as the last build.
- ISR (incremental static regeneration): render once, serve from cache, and re-render single pages after a time limit (in the background) or when told to. Fresh enough for most content, without a full rebuild.
The catch: freshness
Pure SSG has one big weakness. The HTML is a snapshot of the data at build time. If an editor fixes a typo in the lasagna recipe, the fix does not show until somebody rebuilds and redeploys the site.
For 50 pages that rebuild takes seconds. For 40,000 recipes, or 2 million product pages, it can take an hour or more.
That tension between speed and freshness is what this lesson is about. We will look at how build-time HTML works, how CDNs cache it, how ISR refreshes pages one at a time (on a timer or on demand), what happens to build times at 100,000 pages, and how to tell when content changes too often for any of this to fit.
A useful first question for every page type: is the HTML the same for every visitor for the next minute? If yes, it is a candidate for static generation. If it depends on who is asking, it is not.
The mental model: a print run
Think of SSR as a print shop that prints each newspaper when a reader walks in. Think of SSG as a print run: the paper is printed overnight, bundles go out to newsstands in every town, and a reader picks up a copy from the stand down the street. The printing press is your build. The newsstands are the CDN's edge servers.
ISR adds one thing to this picture: a newsstand that notices its copies are getting old. When a reader takes a copy after the stand's time limit, the reader still gets the old copy right away, and the stand quietly orders a new print of that one page. The next reader gets the new one.
Four moments where rendering can happen
Every rendering strategy is a choice of when the HTML is made. Keep these four moments apart and most confusion goes away.
| Moment | Who renders | What the user waits for | Freshness |
|---|---|---|---|
| Build time (SSG) | Your CI machine, once per deploy | A cached file from the nearest edge | As old as the last deploy |
| Revalidation time (ISR) | A server, once per page per refresh | A cached file; the refresh runs in the background | As old as the time limit or the last on-demand refresh |
| Request time (SSR) | A server, once per request | The server's data fetching and render | Fresh on every request |
| In the browser (CSR) | The user's device | JavaScript download, run, and API calls | Fresh, but late |
Notice that SSG and ISR do not change what happens in the browser. The HTML that arrives is the same kind of HTML that SSR sends. If the page uses React, Vue, or Svelte, the browser still downloads the JavaScript and hydrates the page (attaches event handlers to the existing HTML), with the same cost the SSR lesson described. Static generation moves the server work, not the client work.
What "static" does and does not mean
"Static" describes how the HTML is produced, not how the page behaves. A statically generated page can have a search box, a chat widget, and live prices. Those parts run in the browser after the page loads, or are filled in by small API calls. The HTML shell is the same for everyone; the dynamic bits come later.
- Static: the HTML bytes for a URL are the same for every visitor until the next build or refresh.
- Not implied: no JavaScript, no interactivity, or no data. A static product page can still fetch stock levels from an API.
- Not allowed: anything that depends on the request, like a cookie, a header, or the user's location, inside the static HTML. If you read a cookie during the render, the page cannot be static (frameworks usually switch it to per-request rendering, or refuse to build it).
Check your understanding · Exercise 1
Which of these pages can be generated at build time and served from a CDN as-is? Select all that apply.
Building a static site, step by step
Let's follow a small blog with 300 posts through a build and a first visit. The framework could be Next.js, Astro, Nuxt, SvelteKit, Gatsby, or Hugo; the steps are the same and only the names change.
The build
- List the pages. The build asks your code which URLs exist. For a route like
/blog/[slug], you return the 300 slugs from the CMS. In the Next.js App Router this isgenerateStaticParams; in the Pages Router it wasgetStaticPaths; Astro usesgetStaticPaths; SvelteKit usesentriesplusexport const prerender = true. - Fetch the data. For each page, the build fetches what the page needs: the post body, the author, related posts. This runs on the build machine with full access to private APIs and secrets, which never reach the browser.
- Render to HTML. The framework runs your components once per page and writes
/blog/how-caching-works/index.html(or similar). Frameworks with client-side navigation also write a small data file per page, so moving between pages in the app does not need the full HTML again. - Bundle the assets. JavaScript, CSS, fonts, and images get content hashes in their names, like
app.3f9a1c.js. A changed file gets a new name, so old copies in caches can never be confused with new ones. - Upload and switch. The deploy uploads every file, then switches the site to the new set in one step. Good hosts make this switch atomic: visitors see the old version or the new one, never half of each.
// app/blog/[slug]/page.tsx (Next.js App Router)
export async function generateStaticParams() {
const posts = await cms.listPosts({ fields: ["slug"] });
return posts.map((p) => ({ slug: p.slug }));
}
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await cms.getPost(slug);
return <Article post={post} />;
}
The first visit
A reader in Mumbai opens /blog/how-caching-works. The request goes to the nearest CDN edge server. If the edge has the file cached, it answers in about 20 to 40 ms: one short round trip and a file read.
If the edge doesn't have it yet (a cache miss), it asks a bigger regional cache or the origin storage, keeps a copy, and answers. Every later reader near that edge gets the cached copy.
Compare the time to first byte. SSR from a single region in Virginia to Mumbai costs a long round trip (about 200 ms) plus the render (say 150 ms). The static file from a Mumbai edge costs about 30 ms. That difference shows up directly in First Contentful Paint and Largest Contentful Paint, the two metrics most users feel as "how fast did the page appear".
From there, the browser does what it does for any HTML: it parses, fetches the hashed CSS and JS (also from the edge), paints, then hydrates. So the first paint is fast, and the time to interactive depends on how much JavaScript you ship, exactly as with SSR.
Caching static pages at the CDN
A static file is only fast if a cache close to the user holds it. Two kinds of files come out of a build, and they need opposite caching rules: the HTML, whose URL never changes, and the hashed assets, whose URL changes every time their content does.
Hashed assets: cache forever
A file named app.3f9a1c.js will never hold different bytes. If the code changes, the build writes app.8b20e4.js instead and the new HTML points at it. So the safest rule is also the most aggressive one: Cache-Control: public, max-age=31536000, immutable. Browsers and CDNs keep it for a year and never even ask whether it changed.
HTML: short in the browser, long at the edge
The HTML for /blog/how-caching-works keeps its URL across deploys, so a browser that caches it for a day would show yesterday's post and point at yesterday's assets. The usual answer is to split the rule in two: tell browsers to check every time, and let the CDN hold the file for a long time, because you can empty the CDN on demand and you can't empty a visitor's browser.
max-age=0, must-revalidate: browsers must check before reusing. With anETag(a fingerprint of the file) the check is a cheap304 Not Modifiedfrom the edge.s-maxage=31536000: shared caches (CDNs) may keep it for a year.s-maxageapplies only to shared caches and overridesmax-agethere.- A purge on every deploy: hosts like Vercel, Netlify, and Cloudflare Pages invalidate the HTML for you when a new deploy goes live. If you run your own CDN in front of a bucket, the purge is your job.
- Some CDNs read a separate header,
CDN-Cache-Control(RFC 9213), so you can give the CDN its own rule without touching what browsers see.
CDN-Cache-Control; s-maxage in Cache-Control does the same job for any shared cache.stale-while-revalidate at the edge
The stale-while-revalidate directive (RFC 5861) is the HTTP-level twin of ISR. Cache-Control: s-maxage=60, stale-while-revalidate=86400 tells a cache: the copy is fresh for 60 seconds; after that, for up to one more day, you may keep serving the old copy while you fetch a new one in the background. Only when both windows have passed must a visitor wait for the origin.
This is why ISR and CDNs fit together. When Next.js serves an ISR page itself (for example on your own Node server), it sends an s-maxage that matches the page's time limit together with a stale-while-revalidate directive, so any standard CDN in front gets roughly the same behavior. Managed hosts go further and keep the regenerated page in their own edge cache, so the refresh reaches every region.
Check your understanding · Exercise 2
You deploy a fix to a static blog. Some readers still see the old post an hour later. Which header setups could cause this? Select all that apply.
Incremental regeneration on a timer
Incremental static regeneration gives each static page a time limit. When the limit has passed and someone asks for the page, the server serves the old page right away and renders a fresh one in the background. Only that one page is rebuilt; the other 39,999 recipes are untouched. That is the "incremental" part.
// Next.js App Router: the whole route refreshes at most once an hour
export const revalidate = 3600;
// Or per data request
const res = await fetch(`${API}/recipes/${slug}`, {
next: { revalidate: 3600 },
});
// Next.js Pages Router: returned from getStaticProps
return { props: { recipe }, revalidate: 3600 };
// Nuxt: in nuxt.config, per route pattern
routeRules: { "/recipes/**": { isr: 3600 } }
A refresh, minute by minute
Take a recipe page with revalidate = 60, built at 10:00:00. An editor changes the cooking time at 10:00:20.
- 10:00:40, visitor A: the page is 40 seconds old, still fresh. A gets the build-time page with the old cooking time. Nothing else happens.
- 10:01:30, visitor B: the page is 90 seconds old, past its limit, so it is stale. B still gets the old page instantly. B's request starts a background render.
- 10:01:31: the background render finishes in about 400 ms and replaces the cached page. The clock restarts.
- 10:01:45, visitor C: gets the new page with the corrected time.
Read that again, because interviewers check it. The visitor who arrives after the time limit does not get fresh content; they cause it. And a page nobody visits is never refreshed: the first visitor after a quiet week gets week-old HTML, and only the visitor after them sees the update. So the oldest page anyone can be served is the time limit plus the gap until the next visit, and an edit first shows to the visitor after that one.
What the framework guarantees
- Nobody waits. Every visitor gets a cached page, stale or fresh. The render happens off the request path.
- One render at a time. If 500 visitors hit a stale page in the same second, the server starts one background render, not 500. This protects your CMS from a stampede.
- Failure keeps the old page. If the render throws (the CMS is down, an API times out), the old page stays in the cache and the server tries again on a later request. Visitors see slightly old content instead of an error page.
The last point is a real strength over SSR. With SSR, a CMS outage turns every page into a 500 error. With ISR, the site keeps serving the last good version and nobody notices unless they are waiting for an edit.
Clock: 0 s. CMS has v1. Cache holds v1, rendered at 0 s. Renders so far: 0.
The page was built at 0 s as v1. Visit it, let time pass, and publish edits to see what each visitor gets.
Where the regenerated page lives
The regenerated page has to be stored somewhere every server can read. On a managed host it goes into a shared, global cache, and the new version reaches every region. When you self-host, Next.js by default keeps it on the local disk and in memory of each server process.
That default bites when you run several servers behind a load balancer. Each instance refreshes on its own schedule, so two visitors one second apart can get two different versions of the same page. The fix is a shared cache handler (Next.js has a cacheHandler setting for this) backed by something like Redis or object storage, so all instances read and write one copy. Without it, an on-demand revalidation call only reaches the instance that happens to receive it.
Think first
A pricing page has revalidate = 300. The price changes at 09:00. The page gets one visit every 20 minutes, the last one at 08:58. When does a visitor first see the new price?
Whatever happened at 08:58, the cached copy holds the old price, and after 09:00 nothing refreshes it until a request arrives.
The next visit, at 09:18, gets the old price instantly and triggers the background render. The visit after that, at 09:38, is the first to see the new price: 38 minutes after the change, with a 5-minute limit.
On low-traffic pages the time limit says little about real freshness. Use on-demand revalidation when the change event is known, which is the next chapter.
On-demand revalidation
A timer is a guess about when content changes. Set it short and you re-render pages that didn't change; set it long and an urgent fix waits. But you usually know exactly when content changes: an editor clicks Publish. On-demand revalidation turns that event into a refresh of exactly the pages it affects.
The webhook flow
- The editor publishes a change in the CMS (Contentful, Sanity, Strapi, a custom admin).
- The CMS sends a webhook: an HTTP
POSTto a route in your app, with the changed item's id and type. - Your route checks that the call is genuine (a shared secret or a signature), then works out which pages show that item.
- It tells the framework to expire those cached pages. In Next.js that is
revalidatePathfor a URL orrevalidateTagfor a label attached to data. - Nothing renders yet. The next request for each page triggers the render, gets the new version, and the cache stores it.
revalidateTag in a loop.// app/api/cms-webhook/route.ts
import { revalidateTag } from "next/cache";
export async function POST(req: Request) {
const body = await req.text();
if (!verifySignature(body, req.headers.get("x-cms-signature"))) {
return new Response("Forbidden", { status: 403 });
}
const { type, id } = JSON.parse(body);
// e.g. "product:42". Next.js 16 takes a second argument; { expire: 0 }
// makes the next visitor wait for a fresh render. Older versions take only the tag.
revalidateTag(`${type}:${id}`, { expire: 0 });
return Response.json({ ok: true });
}
// Where the data is read, the fetch carries the tag
await fetch(`${API}/products/42`, { next: { tags: ["product:42"] } });
Paths or tags?
revalidatePath("/products/42") refreshes one URL. It is easy to reason about, but you must know every URL that shows the product. revalidatePath("/products/[id]", "page") refreshes every page of a route at once, which is simple and heavy.
Tags flip the question around. Instead of listing pages, you label data. Product 42 appears on its own page, on the "Running shoes" category page, on the home page's "Trending" strip, and in a comparison page. If every one of those reads the product through a fetch tagged product:42, one revalidateTag("product:42") refreshes all four, including pages you forgot existed.
The API surface is still moving. Recent Next.js versions add a caching directive ("use cache", with cacheTag to label the cached work and cacheLife to set its lifetime), stable since Next.js 16. Version 16 also gave revalidateTag a second argument: a cache profile such as "max", or { expire: 0 }.
That argument decides what the next visitor gets. With "max", they get the old page once more while it refreshes in the background, as with a timer; with { expire: 0 }, they wait for the new one. Server Actions that must read their own write right away can call updateTag instead. The idea stays the same across versions: label cached work, then invalidate by label.
Don't forget the CDN in front
If you put your own CDN in front of the app, refreshing the framework's cache is not enough: the CDN still holds its copy until its own rule expires. CDNs offer the same tag idea for this. Fastly calls them surrogate keys and Cloudflare calls them cache tags: the origin labels each response with a header, and one API call purges every response with that label in seconds.
Check your understanding · Exercise 3
Your on-demand revalidation route works in testing. Which of these are real risks in production? Select all that apply.
Build times at 100,000 pages
Pure SSG has a cost that grows with the site: every deploy renders every page. For a 300-post blog that is a rounding error. For a store with 100,000 products it decides whether you can ship a fix before lunch.
The arithmetic
Say each page takes 80 ms to build: about 50 ms waiting for the CMS and 30 ms to render. One worker needs 100,000 x 80 ms = 8,000 seconds, about 2 hours 13 minutes. Eight parallel workers bring that to about 17 minutes, if nothing else limits you.
Something else usually does. If the CMS API allows 50 requests per second, 100,000 single-item calls take 2,000 seconds (33 minutes) no matter how many workers you add. Then 100,000 HTML files of about 40 KB each are 4 GB to upload, and many hosts cap build minutes or file counts. A one-word typo fix now costs half an hour and a full deploy.
About 33 min per deploy, limited by the CMS rate limit. 100,000 pages built and about 4,000 MB of HTML uploaded.
Build the head, render the tail on demand
Traffic on large sites is very uneven. On a typical store, a few thousand products get most of the views, and the long tail of 95,000 others gets a handful each. So build the head ahead of time and let the tail render the first time someone asks, then cache it like any static page.
// app/products/[id]/page.tsx
export const revalidate = 86400; // safety net: refresh at least daily
export const dynamicParams = true; // unknown ids render on first request (the default)
export async function generateStaticParams() {
// only the 2,000 most viewed products are built at deploy time
const top = await analytics.topProducts(2000);
return top.map((id) => ({ id: String(id) }));
}
Fallback pages: what the first visitor sees
When a page was not built and its first visitor arrives, something has to be shown while it renders. Frameworks give you two choices, and the Next.js Pages Router named them with its fallback option:
- Blocking (
fallback: "blocking", and the App Router's default behavior): the first visitor waits for a server render, exactly like SSR, and gets full HTML. Crawlers and link previews always get real content. Everyone after gets the cached file. - Fallback shell (
fallback: true): the first visitor gets a generic loading page right away, and the browser fills it in once the data arrives. It feels fast, but it is a client-side render for that visitor: a skeleton, a layout shift risk, and a page whose HTML has no content. - None (
fallback: false, ordynamicParams = false): pages not in the build are a 404. Right for a closed set, like 300 blog posts, where an unknown slug really is an error.
In the App Router, a loading.tsx file lets the blocking render stream a loading state first and the content right after, which gives a middle ground. That is streaming SSR, covered in its own lesson.
Decision A store has 100,000 products and wants fast pages and deploys under 10 minutes. What should the build render?
What we pick: Build the head and render the tail on demand. It keeps deploys short, keeps popular pages instant, and the cost lands only on pages almost nobody visits. Pure full builds fit sites up to a few thousand pages.
Check your understanding · Exercise 4
A docs site has 20,000 pages and builds in 40 minutes, mostly waiting on one rate-limited CMS call per page. Which changes actually shorten the time to get a typo fix live? Select all that apply.
Failure modes and edge cases
Static pages fail quietly. There is rarely an error page; instead somebody sees the wrong content, and nobody knows for how long. Here are the problems that show up in production, how you notice them, and the fix for each.
| Symptom | Likely cause | Fix |
|---|---|---|
| A deleted product still loads | The page is cached, and the refresh either never ran or rendered the old data | Send delete events to the webhook too, and call notFound() when the item is gone so the refresh caches a 404 |
| The list page and the detail page show different prices | Only one of the pages was revalidated | Tag the shared data and revalidate the tag, not single paths |
| Visitors see old and new versions in turn | Several server instances, each with its own local cache | A shared cache handler, or a host with a global cache |
| One user sees another user's name | Personal data rendered into cached HTML | Keep personal parts out of the static render; load them on the client or render that route per request |
| Console warns of a hydration mismatch | The build rendered a time-based value, like "posted 3 hours ago" | Render absolute dates in the HTML, or compute relative times in the browser after hydration |
| Editors can't preview drafts | Drafts aren't published, so static pages never include them | A preview mode that bypasses the cache for signed-in editors (Next.js calls it Draft Mode) |
The worst one: caching personal data
Most of these bugs are embarrassing. One is a security incident. If a page reads a session cookie while rendering, and the result gets stored in a shared cache, the next visitor gets the first visitor's page: their name, their cart, maybe their address. This has happened to real companies through CDN rules that cached HTML too broadly.
Frameworks protect you in the common case. In Next.js, calling cookies() or headers() during a render makes that route dynamic, so it is not cached as a static page. The danger comes from layers the framework can't see: a CDN rule like "cache everything under /account", or a custom cache key that ignores the cookie. Treat any rule that caches HTML as a security review item.
What happens on deploy
A deploy creates a new set of build-time pages. On most setups, pages that were regenerated after the last deploy go back to their build-time version (built from the data at build time, so usually still current), and pages that were rendered on demand start cold. Right after a big deploy, the origin can see a burst of first-visit renders. Pre-building the head of your traffic softens that burst.
How you notice
- Look at the response headers. The standard
Ageheader says how many seconds a cached copy has been held. Many hosts add their own cache status header, such as a hit, miss, or stale value. - Log every background render and every webhook call with its result. A failed refresh keeps the old page, so without a log the failure is invisible.
- Alert on webhook failures from the CMS side as well: most CMSs show a delivery log with status codes.
- Add a build timestamp or content version in a meta tag, so support can tell which version a user was looking at.
Check your understanding · Exercise 5
A news site uses ISR with revalidate = 120. Readers report that an article's headline sometimes changes back to an older one on refresh. What could explain it? Select all that apply.
When content changes too often
Static generation pays off when one render serves many visitors. The useful number is the reads per change: how many times a page is viewed between two edits. A docs page read 5,000 times between edits is perfect for it. A stock ticker that changes every second and is read twice a second is not: almost every request would need a new render anyway.
Signs that static is the wrong fit
- The time limit you'd need is shorter than the gap between visits. With
revalidate = 5on a page visited once a minute, every visit is stale and triggers a render. That is SSR with an extra step and stale content on top. - The set of pages is unbounded. Search results, filter combinations, and user-generated URLs can't be listed at build time, and caching each one fills the cache with pages nobody opens twice.
- The content is per user. A feed, a cart, a dashboard. Static HTML is shared by definition.
- Correctness matters more than speed. A checkout total or a bank balance must never be even a few seconds old.
The usual fix: split the page
Most pages are not all one thing. A product page has a name, photos, and a description that change monthly, a price that changes daily, and a stock level that changes by the minute. Making the whole page dynamic because of the stock level throws away the speed of the rest.
So split by how fast each part changes. Keep the slow parts in the static HTML. Fetch the fast parts from a small API in the browser after load, or push them with server-sent events if they change constantly. The static HTML is what users and crawlers see first; the live parts fill in a moment later, in space reserved for them so nothing jumps.
Frameworks are moving this split into the server. Next.js partial prerendering (part of its Cache Components model in recent versions) serves a static shell from the cache and streams the dynamic holes into the same response. Astro's islands keep most of the page as static HTML with small interactive parts. Both get their own lessons: streaming SSR, and islands and partial hydration.
| Content | How often it changes | A good fit |
|---|---|---|
| Docs, blog posts, marketing pages | A few times a day at most | SSG, plus on-demand revalidation on publish |
| Product description and photos | Weekly | ISR with on-demand refresh and a daily safety timer |
| Price | Daily or hourly | In the static page, refreshed by tag when it changes |
| Stock level | Every few minutes | Fetched in the browser, or a dynamic hole in a static shell |
| News home page | Every few minutes | ISR with a 30 to 60 second limit, or SSR with a short CDN cache |
| Live scores, prices on a trading screen | Every second | Static shell plus a live stream |
| Cart, account, feed | Per user | Not static: SSR per request or CSR |
Decision A news home page changes every two to five minutes and gets 3,000 views per minute. How should it be rendered?
What we pick: ISR with a short limit plus on-demand refresh, or SSR behind a short CDN cache: both give cached speed with seconds of staleness. Avoid moving the main content to the client, because the headlines are the page.
Think first
A product page uses revalidate = 3600. The item sells out at 10:05. What does a visitor at 10:20 see, and how would you fix it without making the page dynamic?
They probably see "In stock": the cached page can be up to an hour old, and refreshes only after the limit and a visit.
Fix the event, not the timer: when stock reaches zero, the inventory service calls the revalidation route for that product's tag. The page refreshes in seconds.
Better still, take the stock level out of the static HTML and fetch it in the browser, and let the add-to-cart call check stock on the server, which must happen anyway.
In real frameworks and products
Every major meta-framework supports static output, and most now let you mix static, regenerated, and per-request routes in one app. The names differ; the three questions are the same: which pages exist, when does each one refresh, and who tells it to.
| Framework | Static pages | Refresh after launch |
|---|---|---|
| Next.js (App Router) | Static by default when nothing reads the request; generateStaticParams lists dynamic routes | revalidate time limits, revalidatePath, revalidateTag |
| Next.js (Pages Router) | getStaticProps and getStaticPaths with a fallback mode | revalidate in getStaticProps, res.revalidate() for on-demand |
| Nuxt | prerender in route rules or at build | Route rules such as isr and swr with a time in seconds |
| Astro | Static by default; a route opts out with export const prerender = false | Depends on the host adapter; often rebuilds or per-request routes |
| SvelteKit | export const prerender = true per route | Host adapters, for example an ISR expiration setting on Vercel |
| Hugo, Eleventy, Jekyll | Everything, at build time | Rebuild and redeploy |
Who uses which
- Documentation sites are the classic fit. MDN Web Docs is written as Markdown files in a public GitHub repository and built into static pages; many framework docs sites are statically generated too. Edits arrive through pull requests, so a rebuild per merge is natural.
- Marketing sites and blogs built on a headless CMS usually pair SSG with publish webhooks. Editors see changes in seconds without a developer.
- Large catalogs (stores, listings, recipe sites) prebuild their most visited pages and render the long tail on demand, with tag-based refresh when prices or descriptions change.
- News sites often use short time limits or SSR behind a short CDN cache, because the home page changes every few minutes and article pages get updates after publishing.
The hosting side
Pure SSG output is a folder of files, so it can live on any object storage plus CDN: S3 with CloudFront, Cloudflare Pages, GitHub Pages, Netlify. That is the cheapest and most reliable hosting there is, with no server to patch or scale. ISR and on-demand rendering need a server or serverless functions to do the background renders, plus a cache to store them, which is why they are tied more closely to particular hosts or to a self-hosted setup with a shared cache.
Using SSG and ISR in a system design interview
Rendering choices come up in almost every frontend design: a news feed, a product page, a docs site, a booking flow. Bring up static generation when a page type is the same for every visitor and is read far more often than it changes. Say why in one sentence, then move on unless the interviewer digs.
What to say
- Split the page types. "Product pages are the same for everyone, so they're static with ISR. Cart and checkout are per user, so they render per request or on the client."
- Name the freshness budget. "A description can be an hour old; a price must update within a minute of a change; stock is fetched live."
- Name the refresh trigger. "The catalog service fires an event on change; a webhook calls
revalidateTagfor that product, with a daily time limit as a safety net." - Size the build. "100,000 products would take over 30 minutes to build, so we prebuild the top few thousand and render the rest on first visit."
- Point at the risks. Personal data in cached HTML, several server instances with separate caches, and webhooks that get lost.
Draw it, too. A small picture of build, CDN edge, origin, and CMS webhook, with an arrow labelled "stale, refresh in background", says more than a paragraph. If the interviewer pushes on freshness, walk through one edit from publish to the next visitor, with times.
How deep to go by level
| Level | What a strong answer shows |
|---|---|
| Junior | Knows SSG renders at build time and is served from a CDN; can say which pages fit and that content goes stale until a rebuild |
| Mid | Explains ISR's serve-stale-then-refresh behavior correctly, uses on-demand revalidation for edits, sets sensible cache headers for HTML versus hashed assets |
| Senior | Sizes the build and picks what to prebuild, designs tag-based invalidation across pages and the CDN, plans for shared caches, deploy cold starts, and data leaks, and splits pages by rate of change |
Check your understanding · Exercise 6
You are designing a travel site. Which pages are good candidates for SSG or ISR? Select all that apply.
Summary
- Static generation renders a page once at build time and serves the same file from a CDN to everyone: fast, cheap, and resilient, but only for pages that are the same for every visitor.
- It moves server work, not browser work. Hydration and JavaScript cost are the same as with SSR.
- Cache hashed assets forever with
immutable; keep HTML short in the browser and long at the CDN, and purge the CDN on deploy. - ISR serves the stale page and refreshes it in the background. The first visitor after the limit still sees old content; a page nobody visits never refreshes.
- On-demand revalidation, ideally by tag, refreshes exactly the pages a change affects. Keep a long time limit as a safety net for lost webhooks.
- At 100,000 pages, build the most visited pages and render the long tail on first request; batch data fetching during the build.
- Split pages by how fast their parts change, and never put personal data in cached HTML.
In the interview
Questions interviewers ask about this. Answer each out loud first, then compare.
Question 1
How does incremental static regeneration work, and what does the user see right after the time limit passes?
Each static page has a time limit, like 60 seconds. Until it passes, the cached page is fresh and served as-is.
After it passes, the next request still gets the old page immediately, and that request starts a background render. When the render finishes, it replaces the cached copy, and later visitors get the new page.
So the oldest page anyone can be served is the limit plus the gap until the next visit, and the visitor after that one is the first to see an edit. If the render fails, the old page keeps being served. It is the same idea as stale-while-revalidate in HTTP caching.
Question 2
An editor fixes a wrong price and wants it live within seconds. The site uses ISR with a one-hour limit. What do you change?
Keep the hour as a safety net and add on-demand revalidation. The CMS sends a webhook on publish; a route verifies the signature and calls revalidateTag for that product's tag.
Tagging the data instead of listing paths also refreshes the category and home pages that show the same price.
If there's a CDN of our own in front, purge it by surrogate key or cache tag in the same handler, or the CDN keeps serving the old copy.
Question 3
Your store has 100,000 product pages and a full static build takes 40 minutes. How do you fix it?
Find the bottleneck first: often it's one CMS call per page against a rate limit, which batching fixes.
Then stop building everything. Prebuild the few thousand pages that get most of the traffic, and let the rest render on their first request and be cached with ISR. Deploys drop to a few minutes.
Content edits shouldn't need a deploy at all: on-demand revalidation refreshes just the changed pages.
Watch for junk ids: unknown products must return a 404 so bots can't fill the cache.
Question 4
When would you not use static generation, even for a public page?
When it changes about as often as it's read, so every visit would need a new render anyway, like live scores.
When the set of URLs is unbounded, like search results and filter combinations.
When correctness can't tolerate even seconds of staleness, like totals at checkout.
Often the answer is to split the page: a static shell for the slow parts, and live parts fetched or streamed. The page doesn't have to be all one mode.
Question 5
You self-host a Next.js app on four instances behind a load balancer, with ISR. Users report pages flipping between old and new. Why, and what's the fix?
By default each instance keeps regenerated pages on its own disk and in memory, and refreshes them on its own schedule. Requests landing on different instances get different versions.
Configure a shared cache handler, backed by Redis or object storage, so all instances read and write one copy, and make on-demand revalidation reach that shared cache.
Alternatively, put a CDN in front with short cache rules and purge it on change, but the shared cache is the root fix.
Practice it
These design problems lean on static generation and incremental regeneration.
References
- Incremental Static Regeneration (Next.js docs) Time-based and on-demand revalidation in the App Router.
- revalidateTag (Next.js API reference) Check the signature for the version you run.
- revalidatePath (Next.js API reference)
- Rendering on the Web (web.dev) Compares static, server, and client rendering.
- Cache-Control (MDN) s-maxage, immutable, and stale-while-revalidate.
- RFC 5861: HTTP Cache-Control Extensions for Stale Content Where stale-while-revalidate is defined.
- RFC 9213: Targeted HTTP Cache Control The CDN-Cache-Control header.
Read next
- Server-side renderingThe server builds the full HTML for each request, so users see content before any JavaScript runs, then the browser hydrates it: faster first paint and better SEO, paid for with server work, a later first byte, and a second render in the browser.
- Client-side renderingThe server sends an almost empty HTML page plus a JavaScript bundle, and the browser builds the whole page itself: easy to host and great behind a login, but slow to first content and weak for search.
- Streaming server renderingThe server sends a page's HTML in pieces as each part becomes ready, instead of waiting for its slowest data: the layout paints right away, slow sections stream in later, and each piece hydrates on its own.
- CDN cachingKeeping shared copies of your responses on servers near every user, so most requests never travel to your origin: the biggest lever for global speed and for surviving traffic spikes.
- Cache invalidationHow every copy of a piece of data, from the server cache to the CDN to the open tab, learns that it is out of date: by time, by event, or by a new name, in the right order and without a stale read sneaking back in.
- Choosing a rendering strategyHow to pick the rendering strategy for each page by asking who sees it, how often it changes, how interactive it is, and whether strangers must find it, then mixing strategies route by route in one app instead of choosing one for everything.