Choosing Between SSG, DSG, and SSR in Gatsby: A Practical Decision Framework
Gatsby lets you pick SSG, DSG, or SSR per page. Here's a practical framework — with working code — for deciding which rendering mode each route actually needs.
23 Mar 2026, 06:29 UTC

You have a Gatsby site with 500 product pages. Marketing wants real-time inventory counts on every page. Your instinct says "just make it dynamic," but switching everything to server-side rendering would throw away the main reason you picked Gatsby in the first place: pre-built HTML served instantly from a CDN. The better question is which rendering mode each page actually needs — because Gatsby lets you choose per page, not per site.
This post walks through the three modes Gatsby offers — Static Site Generation (SSG), Deferred Static Generation (DSG), and Server-Side Rendering (SSR) — and gives you a concrete way to decide which one fits a given page.
The three modes, in plain terms
SSG is Gatsby's default. At build time, Gatsby runs your GraphQL queries, renders each page to HTML, and writes static files. Nothing runs on a server at request time; a CDN serves the file. Fast, cheap, cacheable — but the data is as fresh as your last build.
DSG is SSG with a lazy twist. You mark a page as deferred, and Gatsby skips building it during the deploy. The first real visitor triggers the build, and every visitor after that gets the cached static file. It exists to solve one problem: build times that grow linearly with page count.
SSR renders the page on a server for every request. You export a getServerData function from the page, and its result becomes props. Data is always fresh, but you now pay server costs and latency on every hit, and you lose the "it's just a file on a CDN" resilience.
A worked example: deferring catalog pages
Say your 500-product catalog is built programmatically in gatsby-node.js. Analytics show 80% of traffic hits the top 100 products. Building all 500 pages on every deploy wastes build minutes on pages nobody visits. Defer the long tail:
// gatsby-node.js — runs at build time, needs Node access to your Gatsby project
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
const result = await graphql(`
{
allProduct {
nodes { id slug popularityRank }
}
}
`);
result.data.allProduct.nodes.forEach((product) => {
createPage({
path: `/products/${product.slug}`,
component: require.resolve("./src/templates/product.js"),
context: { id: product.id },
// Build the top 100 at deploy time; defer the rest
defer: product.popularityRank > 100,
});
});
});
The defer: true flag is the whole mechanism. To verify it worked, check the build log: Gatsby reports how many pages were built versus deferred. Then request a deferred page in your deployed environment — the first request takes noticeably longer (it triggers the build), and a second request should be fast. Compare response headers in your browser's Network tab if you want to confirm caching behavior on your hosting platform.
When SSR earns its keep
SSR is the right tool when the page content must be computed per request — think personalized dashboards, or pages that depend on request headers or authentication. The API is a single export from the page file:
// src/pages/account.js — runs on the server per request
export async function getServerData() {
const res = await fetch("https://api.example.com/account", {
headers: { Authorization: `Bearer ${process.env.ACCOUNT_API_TOKEN}` },
});
if (!res.ok) {
return { status: 500, props: { error: true } };
}
return { props: await res.json() };
}
export default function AccountPage({ serverData }) {
if (serverData.error) return <p>Something went wrong.</p>;
return <h1>Hello, {serverData.name}</h1>;
}
Two things to check here: the API token must be set as an environment variable in your hosting platform (never commit it), and SSR requires a host that actually runs Gatsby's server functions — a plain static file host cannot serve SSR pages. Verify by loading the page twice with different upstream data and confirming the HTML changes between requests.
The hybrid middle ground you should prefer
Back to the inventory problem. The honest answer is often "none of the above, by itself." Keep the product page as SSG — description, images, reviews — and fetch only the inventory count client-side after hydration. The static shell loads instantly; a small component then calls your inventory API and fills in the number.
The trade-off is layout shift: if the inventory widget pops in and pushes content around, users notice. Reserve fixed space for it (a min-height container or a skeleton placeholder) and the problem mostly disappears. This pattern gives you SSG performance with live data exactly where freshness matters, and it's usually a better answer than converting the whole page to SSR.
A decision checklist
- Content changes only when you publish? SSG.
- Thousands of pages, most rarely visited? SSG with DSG for the long tail.
- Content must differ per request or per user? SSR — but only for those pages.
- Mostly static with one live widget? SSG plus client-side fetching.
One caveat worth repeating: rendering-mode APIs and hosting support have evolved across Gatsby versions, so confirm defer and getServerData behavior against the docs for the major version you're running before committing to an architecture.
The actionable step: audit your routes this week. List each page type, write down how fresh its data truly needs to be, and map each to a mode. Most teams discover that 90% of their site should stay static, and the dynamic remainder is smaller — and cheaper — than they assumed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.