open atlas
↑ Back to track
Next.js, zero to senior NEXT · 01 · 01

SSG, SSR, ISR: who builds the HTML, and when

SSG builds HTML at deploy, SSR per request, ISR serves stale and rebuilds in the background after a revalidate window. The axis: TTFB vs freshness vs infra cost; classic failures — stale pages surviving a deploy and a thundering herd of simultaneous revalidations.

NEXT Middle ◷ 18 min
Level
FoundationsJuniorMiddleSenior
Already know this unit? Take a 1-minute quick check →

Black Friday, 09:00. Marketing flips the discount flag in the CMS and waits for the homepage to show the new prices. Nothing. Ten minutes later, still the old prices — for some users. Others see the new ones. The team redeploys “to flush the cache” and now it’s worse: for a few minutes some product pages 500 because ten thousand requests all hit a page whose cache entry just vanished, and every one of them triggers a full re-render against a CMS that rate-limits at 50 req/s. Nobody on the team could answer the only question that mattered: for this page, who builds the HTML, and when? The homepage was ISR with a 15-minute revalidate window — so the CMS change was invisible until a visitor happened to land after the window expired, per CDN region. The deploy didn’t “flush a cache”; it emptied the ISR cache entirely, turning every first request into an on-demand build. The outage wasn’t a bug. It was a rendering strategy nobody had consciously chosen.

By the end of this lesson you’ll be able to name the rendering mode of any Next.js route and predict exactly which failure it can produce under load.

Three answers to one question

Every rendering strategy is an answer to a single question: at what moment does a server turn your React tree into HTML? SSG (Static Site Generation) answers “at build time”: next build renders the page once, the output is a plain .html file (plus a payload for client navigation), and a CDN serves that file forever — TTFB is whatever the nearest edge node takes, typically 10–50ms, and your origin does zero work per request. SSR (Server-Side Rendering) answers “on every request”: each visitor pays for a fresh render — the server fetches data, runs your components, and streams HTML. You get perfect freshness and access to request-time context (cookies, headers, geo), and in exchange TTFB now includes your data layer: a page that waits on a 200ms database query has a 200ms+ TTFB floor, multiplied by every concurrent user. ISR (Incremental Static Regeneration) is the deliberate compromise: serve the cached static page immediately — even if it’s stale — and if the page is older than its revalidate window, trigger a re-render in the background. The visitor who triggered regeneration still gets the old page; the next visitor gets the new one. This is the stale-while-revalidate pattern applied to whole pages.

In the App Router you don’t pick a mode with a config flag — the route’s behavior falls out of how it fetches data:

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

// ISR: this page is static, re-rendered in the background
// at most once every 60 seconds per path.
export const revalidate = 60;

export async function generateStaticParams() {
  // Pre-render the top 100 products at build time (SSG);
  // other ids render on first request, then cache (ISR on demand).
  const top = await getTopProducts(100);
  return top.map((p) => ({ id: p.id }));
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  return <ProductView product={product} />;
}

// Flip to full SSR instead: every request renders fresh.
// export const dynamic = "force-dynamic";

One file expresses all three: paths from generateStaticParams are SSG, the revalidate export turns the cache into ISR, and force-dynamic (or reading cookies()/headers()) opts the route into per-request SSR. The danger is that this is implicit — adding one cookies() call deep in a shared component silently converts a static route into SSR, and your “static” page starts hammering the origin.

Why this works

Why does ISR serve the stale page instead of making the visitor wait for the fresh one? Because the alternative couples your TTFB to your slowest dependency exactly when it hurts most. If regeneration were blocking, the first visitor after every window expiry would pay the full render cost — and under load, many visitors would arrive in the gap and all wait (or all trigger renders). Stale-while-revalidate decouples the two: read path stays O(serve a file), write path happens once, asynchronously, off the user’s critical path. The price is honesty about staleness: with revalidate = 60, the contract is “users may see data up to 60 seconds old, plus the regeneration time” — and that contract holds per path, per cache node, not globally.

Quiz

An ISR page has revalidate = 300. The CMS content changes at 12:00. A visitor hits the page at 12:04 (last render was 11:58). What do they see, and what happens?

The tradeoff matrix: TTFB, freshness, infra cost

Lay the three strategies on three axes and the decision becomes mechanical. TTFB: SSG wins outright — a file from a CDN edge, 10–50ms, no origin involved. ISR is the same on the hot path (it is a cached file) with occasional background work. SSR is the only one where TTFB includes compute and data fetching: realistically 100–500ms from the origin region, worse cross-continent, and it degrades under load because every request consumes server CPU. Freshness: SSR is always current. ISR is bounded-stale — at most revalidate seconds plus regeneration lag. SSG is frozen until the next deploy; a typo on a fully static marketing page costs you a full build-and-deploy cycle, which on a large site can be 10–20 minutes. Infra cost: SSG is nearly free to serve at any scale — 10 requests or 10 million, the origin does the same work: none. ISR costs one render per path per window, independent of traffic — a beautiful property: 1M views/hour on a revalidate: 60 page is still ~60 renders/hour. SSR costs scale linearly with traffic; it is the only strategy where a traffic spike is also a compute spike, and the only one where you need to think about autoscaling and connection pools to survive the front page of Hacker News.

The honest selection rule is not “which is best” but “what staleness can this route afford”: docs and marketing → SSG; product listings, news, dashboards-for-everyone → ISR with a window matched to the data’s real change rate; anything personalized, paginated by user, or reading cookies → SSR (or, more precisely, dynamic rendering with cached data underneath).

Pick the best fit

A Next.js product listing page shows prices and stock levels that change every few minutes. The team needs sub-100ms TTFB and can tolerate data up to 2 minutes stale. Pick the rendering strategy.

Failure modes: stale-after-deploy and the thundering herd

Two production failures define this topic. First: ISR serving stale after a deploy. The ISR cache is keyed to a build. Self-hosted, the default cache lives on the filesystem of each instance — a new deploy starts with an empty cache (or worse, with several instances each holding a different cache, so users see different versions depending on which pod they hit). On Vercel the cache is shared and survives deploys for unchanged pages, but a changed page resets, and revalidate timers reset with it. Either way the mental model “deploying flushes everything to fresh” is wrong in both directions: some pages stay stale, others lose their cache entirely. The fix for multi-instance self-hosting is a shared cache handler (cacheHandler in next.config.js pointing at Redis or similar) so all pods agree on what’s cached. Second: thundering revalidation. When a popular page’s entry expires — or a deploy empties the cache — every concurrent request in that gap can become a render. Within one Next.js instance, in-flight regeneration is deduplicated; across instances it is not, unless your cache handler implements locking. Ten pods × one expired hot page × a CMS that rate-limits = the Hook’s 500s. Mitigations: stagger windows so thousands of pages don’t expire in sync, prefer on-demand revalidation (revalidatePath from a webhook) for change-driven data so windows can be long, and warm critical paths immediately after deploy instead of letting real users do it.

Quiz

A self-hosted Next.js app runs 8 pods behind a load balancer. Product pages use ISR with revalidate = 60. Users report seeing prices flip back and forth between old and new on refresh. Most likely cause?

Recall before you leave
  1. 01
    Walk through what happens when a visitor hits an ISR page whose revalidate window has expired. Who sees fresh content, and when?
  2. 02
    Compare SSG, SSR, and ISR on TTFB, freshness, and infra cost, and give the selection rule.
Recap

Every strategy answers one question: when does the server turn the React tree into HTML? SSG answers “at build” — next build emits static files, the CDN serves them in 10–50ms, the origin does nothing per request, and the content is frozen until the next deploy. SSR answers “per request” — fresh data and access to cookies and headers, but TTFB now contains your data layer (100–500ms realistic) and compute cost scales linearly with traffic. ISR is stale-while-revalidate for whole pages: serve the cached file immediately even when stale; if the entry is older than its revalidate window, re-render in the background so the next visitor gets the fresh copy. Its cost is one render per path per window regardless of traffic, and its contract is bounded staleness — window plus regeneration lag, per path, per cache node. In the App Router the mode is implicit: generateStaticParams pre-renders paths, a revalidate export turns the cache into ISR, and reading cookies() or forcing dynamic flips the route to SSR — so a single stray cookies() call can silently convert a static route into per-request rendering. The two production failures to hold in mind: ISR-after-deploy, where self-hosted multi-instance deployments hold divergent per-pod filesystem caches (users see versions flip per refresh — fix with a shared cacheHandler) and where a deploy empties caches so first requests pay full renders; and thundering revalidation, where a hot page’s expiry across many instances stampedes the data source — mitigate by staggering windows, preferring on-demand revalidatePath webhooks for change-driven data, and warming critical paths after deploy. Now when you see a page behaving oddly after a deploy — showing the wrong version to some users, or 500ing under a traffic spike — the first question to ask is: what rendering mode is this route in, and who empties its cache?

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.