cf.completefrontend
My progress
FUNDAMENTALS · MODULE 7

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.
A bar chart, to scale, of how many times one recipe page with 30,000 daily visits is rendered in a day. SSR renders it 30,000 times, with a time to first byte of about 200 milliseconds that rises to 3 seconds at peak. ISR with a 60 second limit renders it at most 1,440 times. SSG renders it once per deploy. Both static options answer from the edge in about 30 milliseconds.renders per day for one page, to scaleSSR: render on every request30,000 rendersTTFB 200 ms, 3 s at peakISR, revalidate = 60at most 1,440TTFB about 30 ms from the edgeSSG: render once per deploy1 renderTTFB about 30 ms from the edge
Same page, same visitors. SSR repeats identical work for every request; static generation does it once and lets the CDN answer everyone else.

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.

MomentWho rendersWhat the user waits forFreshness
Build time (SSG)Your CI machine, once per deployA cached file from the nearest edgeAs old as the last deploy
Revalidation time (ISR)A server, once per page per refreshA cached file; the refresh runs in the backgroundAs old as the time limit or the last on-demand refresh
Request time (SSR)A server, once per requestThe server's data fetching and renderFresh on every request
In the browser (CSR)The user's deviceJavaScript download, run, and API callsFresh, 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.

A stack of four layers. At build time, the build machine fetches data and renders HTML once per deploy. At request time, origin storage holds the files and the ISR server and is reached only on a cache miss; the CDN edge keeps a copy near each user and answers in 20 to 40 milliseconds; the browser paints the HTML and then hydrates, with the same JavaScript cost as SSR.Build timeRequest time: the networkRequest time: the deviceBuild (CI)renders HTML onceonce per deployOrigin storagefiles + ISR serveron a missCDN edgea copy near each user20 to 40 msBrowserpaints, then hydratessame JS as SSRearlierlater
Static generation moves rendering to the top layer and lets the edge answer requests. The bottom layer does not change: the browser still runs your JavaScript.

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

  1. 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 is generateStaticParams; in the Pages Router it was getStaticPaths; Astro uses getStaticPaths; SvelteKit uses entries plus export const prerender = true.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
tsx
// 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 build calls generateStaticParams once, then renders the page once per slug. Recent Next.js versions pass params as a promise, hence the await.
A flowchart of a static build for a 300 post blog. The CMS feeds step 1, list the pages, which returns 300 slugs. Step 2 fetches each page's data on the build machine. Step 3 renders 300 HTML files. Step 4 gives the JavaScript and CSS hashed names like app.3f9a1c.js. Step 5 uploads every file and switches the live site to the new set in one atomic step.CMS1. List pages300 slugs2. Fetch dataper page, on CI3. Render HTML300 .html files4. Hash assetsapp.3f9a1c.js5. Upload, then switch atomicallyfilesHTML + assetsold or new, never half
Everything above the last box happens on the build machine, out of sight of users. The atomic switch at the end means a visitor sees the old site or the new one, never a mix.

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 an ETag (a fingerprint of the file) the check is a cheap 304 Not Modified from the edge.
  • s-maxage=31536000: shared caches (CDNs) may keep it for a year. s-maxage applies only to shared caches and overrides max-age there.
  • 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.
Two responses with notes. The HTML page /blog/how-caching-works keeps its URL across deploys and is sent with Cache-Control max-age=0, must-revalidate, so browsers check each time; CDN-Cache-Control max-age=31536000, so the CDN keeps it a year; and ETag "p-91c2", so a check costs a 304. The hashed file app.3f9a1c.js is sent with Cache-Control max-age=31536000, immutable, and is never checked again. A note says the deploy must purge the CDN copy of the HTML./blog/how-caching-works (same URL every deploy)Cache-Control: max-age=0, must-revalidatebrowsers check each timeCDN-Cache-Control: max-age=31536000the CDN keeps it a yearETag: "p-91c2"a check costs a 304/assets/app.3f9a1c.js (new name per change)Cache-Control: max-age=31536000, immutablenever checked againon deploy: purge the CDN's HTML
Opposite rules for opposite files. The HTML is cached hard only where you can purge it (the CDN); hashed files are cached hard everywhere, because a change gets a new name. Here the CDN lifetime sits in 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.

tsx
// 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 } }
Same idea, different spellings. The number is in seconds. In Next.js, when a page and its fetches set different limits, the shortest one decides how often the page refreshes.

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.

  1. 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.
  2. 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.
  3. 10:01:31: the background render finishes in about 400 ms and replaces the cached page. The clock restarts.
  4. 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.

A state machine for one ISR page with three states. Fresh: served as is. When the time limit passes, it becomes Stale: still served at once. When a visit arrives, it moves to Regenerating, where the old page is still served while a background render runs. If the render succeeds, the new page is cached, the clock restarts, and the page is Fresh again. If the render fails, the old page stays and the state goes back to Stale.Freshserved as isStaleserved at onceRegeneratingold page servedtime limitpassesa visitarrivesrender fails: old page staysrender ok: new page cached, clock restarts
No state makes a visitor wait: every arrow happens off the request path. The only way out of Stale is a visit, which is why an unvisited page never refreshes.

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 last 0 visits with revalidate 60: none yet. 0 of 0 visits saw old content.no visits yet0 of 0 visits saw old content, 0 renders
Each box is one visit, oldest on the left: fresh (served from cache inside the limit), stale (old page served at once, refresh started), or waited (a render on the request, after the webhook). Under each: the version seen and the time.

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?

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

  1. The editor publishes a change in the CMS (Contentful, Sanity, Strapi, a custom admin).
  2. The CMS sends a webhook: an HTTP POST to a route in your app, with the changed item's id and type.
  3. Your route checks that the call is genuine (a shared secret or a signature), then works out which pages show that item.
  4. It tells the framework to expire those cached pages. In Next.js that is revalidatePath for a URL or revalidateTag for a label attached to data.
  5. Nothing renders yet. The next request for each page triggers the render, gets the new version, and the cache stores it.
A sequence diagram between the CMS, the app's webhook route, the ISR cache, and a visitor. The editor publishes, and the CMS sends a signed POST to the webhook route. The route calls revalidateTag, and the cache expires the tagged pages. The route answers the CMS with 200. Later a visitor requests /products/42; the cache asks the app to render it, the app returns the new HTML, version 2, and the visitor gets version 2.CMSApp routeISR cacheVisitorPOST, signedrevalidateTagexpired200 oklaterGET /products/42rendernew HTML, v2v2
The webhook only expires the cached pages; the render happens on the next request, which waits for it. A signature check at the route keeps strangers from calling revalidateTag in a loop.
ts
// 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"] } });
Tag the data where you read it, then invalidate the tag when it changes. The signature check stops strangers from forcing re-renders. Check the revalidateTag signature for your Next.js version.

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.

A graph with the data tag product:42 in the middle and four pages that read data under it: /products/42, its own page; /c/running-shoes, the category grid; the home page with its Trending strip; and /compare/42-vs-17, a comparison table. A note says that revalidateTag("product:42") refreshes all four pages.product:42/products/42its own page/c/running-shoescategory grid/home: Trending strip/compare/42-vs-17comparison tablerevalidateTag("product:42")refreshes all four pages
Tags point from data to pages, so you don't need to know every URL that shows a product. Path-based refresh would have to list all four, and the one you forget stays stale.

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.

Build estimate for 100,000 pages with 100% prebuilt: render work 17 min, CMS calls 33 min, against a 10 minute target. The build takes about 33 min, limited by the CMS.render work17 minCMS calls33 min10 min target10 min
Bars to scale. The slower of the two bars is the build time: adding workers shortens only the render bar, batching shortens only the CMS bar, and prebuilding less shortens both.

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.

tsx
// 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) }));
}
Returning an empty list builds no product pages at all: every page renders on its first visit and is then cached. Setting dynamicParams to false turns unknown ids into a 404 instead.

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, or dynamicParams = 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?

Build every page

generateStaticParams returns all 100,000 ids

Every deploy renders and uploads every page

Pros

  • No visitor ever waits for a render
  • Simple mental model: the deploy is the whole site

Cons

  • Deploys take 30+ minutes and hammer the CMS
  • Every fix waits for a full rebuild

Build the head, render the tail on demand

Build the top 2,000 to 5,000 pages by traffic

Other pages render on first request, then cache with a time limit

Pros

  • Deploys in minutes
  • Popular pages are fast from the first second
  • Tail pages cost nothing until someone visits

Cons

  • The first visitor to a tail page waits for a render
  • Needs a running server, not just a file bucket

Build nothing, render everything on demand

generateStaticParams returns an empty list

Every page renders on first visit and is cached

Pros

  • Fastest possible deploys
  • No list of pages to maintain

Cons

  • After each deploy, every page starts cold
  • A traffic spike right after a deploy hits the origin hard

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.

SymptomLikely causeFix
A deleted product still loadsThe page is cached, and the refresh either never ran or rendered the old dataSend 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 pricesOnly one of the pages was revalidatedTag the shared data and revalidate the tag, not single paths
Visitors see old and new versions in turnSeveral server instances, each with its own local cacheA shared cache handler, or a host with a global cache
One user sees another user's namePersonal data rendered into cached HTMLKeep personal parts out of the static render; load them on the client or render that route per request
Console warns of a hydration mismatchThe 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 draftsDrafts aren't published, so static pages never include themA 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 Age header 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 = 5 on 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.

Two wireframes of the same product page. Before: the whole page is rendered per request, because the stock level changes every few minutes. After: the photos, title, description, and price are static HTML from the CDN, refreshed by tag; only a small stock box is fetched live in the browser, in space reserved for it.before: all dynamicwhole page renderedper request, forone stock numberafter: splitphotos and title: staticdescription, price: staticstock: fetched liveTTFB set by the serverTTFB set by the CDN
Before, one fast-changing field makes the whole page dynamic. After, most of the page is static and only the stock box is live, in a slot sized ahead of time so nothing shifts.

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.

ContentHow often it changesA good fit
Docs, blog posts, marketing pagesA few times a day at mostSSG, plus on-demand revalidation on publish
Product description and photosWeeklyISR with on-demand refresh and a daily safety timer
PriceDaily or hourlyIn the static page, refreshed by tag when it changes
Stock levelEvery few minutesFetched in the browser, or a dynamic hole in a static shell
News home pageEvery few minutesISR with a 30 to 60 second limit, or SSR with a short CDN cache
Live scores, prices on a trading screenEvery secondStatic shell plus a live stream
Cart, account, feedPer userNot static: SSR per request or CSR
A decision tree for one page type. Is the HTML the same for every visitor? If no, use SSR or CSR. If yes: can you list the URLs? If no, use SSR with a short CDN cache. If yes: is it read far more often than it is edited? If no, use a static shell with live data. If yes: do you know when it changes? If no, use ISR on a timer. If yes, use SSG or ISR with a webhook for on-demand revalidation.Same HTML for all?noSSR or CSRyesCan you list the URLs?noSSR + CDN cacheyesRead more than edited?noshell + live datayesKnow when it changes?noISR on a timeryesSSG/ISR + webhook
Ask it per page type, not per site. One product can end up with three answers: static for the description, a webhook for the price, live data for the stock.

Decision A news home page changes every two to five minutes and gets 3,000 views per minute. How should it be rendered?

ISR with a short time limit

revalidate = 30

Plus on-demand refresh when an editor publishes a top story

Pros

  • Every view served from cache
  • At most one render every 30 seconds

Cons

  • Breaking news can lag up to 30 seconds without the on-demand hook

SSR with a short CDN cache

Render per request at the origin

Cache-Control: s-maxage=10, stale-while-revalidate=60 so the CDN absorbs most traffic

Pros

  • Works with any CDN and any server stack
  • Easy to add per-region variants through the cache key

Cons

  • Each edge region renders separately, so more origin load than one shared ISR cache

Static shell, headlines fetched in the browser

Static layout and navigation

Headlines from a cached JSON API

Pros

  • Shell is cached for a long time

Cons

  • Headlines are not in the HTML: worse for crawlers and for LCP
  • Two round trips before the main content shows

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?

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.

FrameworkStatic pagesRefresh after launch
Next.js (App Router)Static by default when nothing reads the request; generateStaticParams lists dynamic routesrevalidate time limits, revalidatePath, revalidateTag
Next.js (Pages Router)getStaticProps and getStaticPaths with a fallback moderevalidate in getStaticProps, res.revalidate() for on-demand
Nuxtprerender in route rules or at buildRoute rules such as isr and swr with a time in seconds
AstroStatic by default; a route opts out with export const prerender = falseDepends on the host adapter; often rebuilds or per-request routes
SvelteKitexport const prerender = true per routeHost adapters, for example an ISR expiration setting on Vercel
Hugo, Eleventy, JekyllEverything, at build timeRebuild 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

  1. 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."
  2. 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."
  3. Name the refresh trigger. "The catalog service fires an event on change; a webhook calls revalidateTag for that product, with a daily time limit as a safety net."
  4. 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."
  5. 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

LevelWhat a strong answer shows
JuniorKnows SSG renders at build time and is served from a CDN; can say which pages fit and that content goes stale until a rebuild
MidExplains ISR's serve-stale-then-refresh behavior correctly, uses on-demand revalidation for edits, sets sensible cache headers for HTML versus hashed assets
SeniorSizes 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?

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?

Question 3

Your store has 100,000 product pages and a full static build takes 40 minutes. How do you fix it?

Question 4

When would you not use static generation, even for a public page?

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?

Practice it

These design problems lean on static generation and incremental regeneration.

References

Read next