Build-Time Data Fetching Is the New SSR
Most “dynamic” sites aren’t. They pull from a CMS or an API, render the result, and serve the same HTML to every visitor for the next hour. Then a server somewhere does that exact same work, on demand, for every request — paying CPU, latency, and a database connection for output that hasn’t changed.
That’s not dynamic. That’s a cache miss with extra steps.
Build-time data fetching collapses the whole pipeline. Pull from the API once, when the site builds. Render every page to HTML. Ship the result to a CDN. The “server” you used to pay for is gone, and the CDN was already there.
The Pattern
Pre-rendering at build time isn’t new — every static site generator does it. What changed is that the data sources got good. Headless CMSes ship REST and GraphQL endpoints. Stripe, Shopify, Notion, Airtable, Sanity, Contentful — all of them respond to a build-time fetch the same way they’d respond to a request handler. The runtime layer was the only thing in between, and most of the time it wasn’t earning its keep.
A modern build-time fetch looks like this in Next.js:
// app/products/page.tsx
export default async function ProductsPage() {
const products = await fetch("https://api.shopify.com/products", {
headers: { Authorization: `Bearer ${process.env.SHOPIFY_TOKEN}` },
}).then((r) => r.json());
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.title} — ${p.price}</li>
))}
</ul>
);
}
No getServerSideProps. No API route in front of the API. The fetch runs once, at build time, in the same Node process that’s emitting the HTML. The output is a flat file.
Astro is even cleaner — top-level await in the frontmatter, no ceremony. Eleventy and Hugo do it through data files. The mechanism varies; the principle doesn’t.
When Build-Time Wins
The decision is almost always about freshness, not about scale. Three questions:
- How often does the data change?
- How fast does a stale version hurt the user?
- How long does a rebuild take?
If your data changes hourly and a 10-minute lag is harmless, build-time wins outright. Marketing sites, documentation, blog content, product catalogs, pricing pages, conference schedules, recipe databases, real estate listings — almost all of it. The data has a freshness window measured in minutes or hours, not milliseconds.
SSR earns its place when the freshness window is shorter than your build time, or when the response is genuinely per-user. A logged-in dashboard. A live order status. A search result. Those are real. The marketing site for the SaaS that serves the dashboard is not.
The honest test: if you removed your SSR layer and ran a build every 15 minutes instead, would anyone notice? If the answer is no, you’re paying for a runtime you don’t need.
ISR: The Middle Ground That Usually Isn’t
Incremental Static Regeneration is the compromise position — pre-render at build, then revalidate stale pages on demand. Next.js made it famous. It’s genuinely useful in a narrow band: when you have thousands of pages, most of which are rarely visited, and you can’t afford to rebuild all of them on every content change.
But ISR brings back the runtime. You need a server (or serverless function) that can intercept requests, check freshness, regenerate, and write back to a cache. That’s infrastructure, billing, observability, and a new failure mode — the first request after expiry pays the regeneration cost. Cold starts on a serverless platform make it worse.
For a 50-page marketing site, ISR is a Rube Goldberg machine. Just rebuild. A modern Next.js or Astro build for a content site of that size completes in under a minute. Render, Netlify, and Vercel all support build hooks — your CMS pings a webhook on publish, the site rebuilds, the CDN purges. The whole loop runs in 60 seconds, costs nothing per request, and has zero runtime to monitor.
The threshold where ISR starts winning isn’t pages — it’s page-changes-per-build-minute. If your build takes 8 minutes and you publish 30 times a day, full rebuilds choke. If your build takes 45 seconds and you publish 5 times a day, ISR is overhead.
The Webhook + Rebuild Loop
The piece most people miss: build-time data fetching only works if rebuilds are cheap and triggered automatically. Manual rebuilds turn a static site into a stale site.
The pattern:
- CMS publishes content → fires a webhook
- Webhook hits a build hook URL on Render / Netlify / Vercel
- CI runs the build, fetches the latest data, emits HTML
- CDN purges and re-warms automatically on deploy
Every major host gives you a build-hook URL with no auth — paste it into your CMS’s webhook settings and you’re done. Sanity, Contentful, Storyblok, and Hygraph all have native deploy webhook integrations. For Shopify or Stripe, a tiny serverless function in between filters which events should trigger a rebuild (don’t rebuild on every cart event — only on product or pricing changes).
The fastest sites I’ve shipped have a 40-second build and a webhook chain measured in seconds. Editor publishes, refreshes their browser a minute later, and the new version is live everywhere on the planet. No SSR layer ever entered the picture.
What You Give Up
Build-time fetching is not a free lunch. The tradeoffs are real, just smaller than the runtime tax:
- No per-request personalization. If the page differs by user, build-time can’t render it. Hydrate the personalized slice on the client, or wrap it in an edge function. (See the edge functions post for that pattern.)
- No second-by-second freshness. Stock tickers, live scores, auction bids — wrong tool. Use a runtime.
- Build time becomes a budget. A 20-minute build for a 10-page site is a smell. Cache the slow fetches, parallelize the fast ones, and treat build duration as a metric you watch.
- Secrets live in CI, not in a server. Your build environment needs the API tokens. That’s a different security model than runtime secrets — usually simpler, but worth knowing.
None of those are dealbreakers for the use case. They’re constraints that tell you which pages should be static and which shouldn’t.
The Default Should Have Flipped Already
For most of the last decade, “build a website” defaulted to a runtime. Rails, Django, WordPress, Laravel, Next.js with getServerSideProps — the assumption was always that a server would assemble the page on demand, with static export as a special case for blogs and brochures.
That default is wrong now. The data sources are static-friendly. The hosts give you build hooks. The CDNs are everywhere. Build times for content-heavy sites have collapsed. The only reason to default to a runtime is if the page genuinely can’t be pre-rendered — and that’s the exception, not the rule.
Ship static. Fetch at build. Let the CDN serve the cached HTML it was going to cache anyway. If a page later proves it needs a runtime, peel that one page off and put an edge function in front of it. Don’t build the whole site on the assumption that every page is the exception.
The runtime tax was always optional. Most sites should stop paying it.