# Content Delivery Network & How to scale frontends

"Scaling the frontend" splits cleanly into two problems that are nothing alike. A **static** frontend is just prebuilt files, and files scale almost for free. A **dynamic** frontend renders HTML on a server for each request, which costs compute and is where the actual scaling work lives. And sitting in front of both is the single biggest lever you have: a **Content Delivery Network**. This post covers how to scale each kind of frontend and how a CDN carries the load - the [caching](/concepts-you-should-know-about-system-designing-part-1) chapter, applied to the edge.

## Static frontends scale like files, because they are files

A static frontend - a site built ahead of time with static site generation (SSG), or a single-page app whose build output is HTML, CSS, and JS - produces the same bytes for every user. There is no per-request computation, so "scaling" it means one thing: serve the files from somewhere that is cheap, distributed, and close to users. That somewhere is **object storage plus a CDN**. You upload the build to object storage (the same S3-style storage you would hand out [presigned URLs](/what-are-presigned-urls) for), point a CDN at it, and you are effectively done - the CDN replicates your files to edge locations worldwide and serves them from memory near the user. This scales to enormous traffic on a rounding error of a budget, because you are not running servers, you are handing out cached files.

The techniques that make static delivery even cheaper are all about serving fewer, smaller, more-cacheable bytes:

- **Hash your asset filenames** (`app.9f2a.js`) and mark them immutable with `Cache-Control: max-age=31536000, immutable`. The content never changes for a given hash, so browsers and the CDN can cache it forever; a deploy just produces new filenames.
- **Code-split** so a route ships only the JS it needs, and **compress** with gzip or Brotli at the edge.
- **Optimize images** (modern formats, responsive sizes) and **lazy-load** below-the-fold assets.

Each of these cuts origin load and transfer size, which is what "scale" means on the static side.

## Dynamic frontends cost compute, so you cache and spread it

A dynamic frontend renders HTML per request - server-side rendering (SSR) - because the page depends on data or the user. That HTML is real work on a real server, so the naive version does not scale: every request hits your render tier. You attack it on three fronts.

**Render statelessly and scale horizontally.** An SSR server holds no per-request state, so run many identical instances behind a load balancer and add instances as traffic grows. This is standard backend scaling applied to the render tier.

**Cache the rendered output.** Most "dynamic" pages are not unique per user - a product page is the same for everyone for minutes at a time. So cache the rendered HTML at the CDN edge and serve it without re-rendering. [Incremental Static Regeneration (ISR)](https://vercel.com/blog/how-to-choose-the-best-rendering-strategy-for-your-app) is the pattern that formalizes this: serve a cached static page, and regenerate it in the background on a schedule or on demand, so you get static-fast delivery with fresh-enough content. Pair it with the `stale-while-revalidate` cache directive, which serves the stale copy instantly while fetching a fresh one behind the scenes.

**Render at the edge.** Instead of one central render tier, run the SSR at the CDN's edge locations so the HTML is generated near the user - cutting the round-trip to a distant origin. Modern frameworks lean on this together with streaming (React Server Components) to send HTML as it is produced.

The real lesson is that rendering is a **per-route** decision, not a global one. Use SSG or ISR for content that is the same for everyone (marketing, product, blog), SSR only for pages that genuinely need fresh, request-specific data, and client-side rendering for authenticated, highly interactive views. Frameworks like [Next.js](https://nextjs.org/) and [Astro](https://astro.build/) let you mix these per page - lean static, and reach for SSR only where you must.

## The CDN, in depth

A **CDN** is a globally distributed network of caching servers (edge locations, or PoPs) that sit between users and your origin. A user's request is routed - usually via anycast or DNS - to the nearest edge, which serves a cached copy if it has one and only contacts your origin on a miss. That is the whole game: turn N requests against your origin into a handful, served the rest of the time from an edge near the user. [Cloudflare](https://www.cloudflare.com/), [Fastly](https://www.fastly.com/), and [AWS CloudFront](https://aws.amazon.com/cloudfront/) are the common providers.

What you actually control:

- **What gets cached and for how long** - via the [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header (`max-age`, `s-maxage` for the CDN specifically, `immutable`, `stale-while-revalidate`). This is the dial for freshness vs origin offload.
- **The cache key** - by default the URL, but you can vary on query string, headers, or cookies. Getting this wrong either caches too little (everything misses) or serves one user's page to another.
- **Invalidation / purge** - when content changes, you purge the key so the edge refetches. As always, invalidation is the hard part; content-hashed asset names sidestep it, and on-demand purges handle the rest.
- **Origin shield** - a designated mid-tier edge that all other edges fetch through, so your origin sees one request per miss instead of one per edge.

A CDN also does more than cache: it terminates TLS at the edge, speaks HTTP/2 and HTTP/3 to the user, absorbs DDoS traffic and runs a WAF before requests ever reach you, and can edge-cache even dynamic HTML with short TTLs plus `stale-while-revalidate`. For most sites, putting a CDN in front is the highest-leverage scaling change available, static or dynamic.

## The full architecture

Individually the pieces are simple; the point is how they combine. Put them together and one picture describes the whole system - the edge in front, a stateless render tier at the origin, and object storage holding the build output:

![Combined frontend scaling architecture: users reach CDN edge PoPs via anycast; the edge serves cached static assets and ISR or SWR HTML, falls back to object storage on a static miss and to the origin load balancer plus stateless SSR render servers (which call backend APIs) on a dynamic or stale-HTML miss; a CI build publishes hashed immutable assets to object storage](https://mermaid.ink/img/Zmxvd2NoYXJ0IExSCiAgICBVW1VzZXJzXQogICAgc3ViZ3JhcGggRWRnZVtDRE4gZWRnZSBQb1BzXQogICAgICAgIENETlsiQ2FjaGU6IHN0YXRpYyBhc3NldHMgKyBJU1IgLyBTV1IgSFRNTCJdCiAgICBlbmQKICAgIHN1YmdyYXBoIE9yaWdpbltPcmlnaW5dCiAgICAgICAgTEJbTG9hZCBCYWxhbmNlcl0KICAgICAgICBTMVtTU1IgcmVuZGVyIHNlcnZlcl0KICAgICAgICBTMltTU1IgcmVuZGVyIHNlcnZlcl0KICAgICAgICBBUElbQmFja2VuZCAvIEFQSXNdCiAgICAgICAgTEIgLS0+IFMxCiAgICAgICAgTEIgLS0+IFMyCiAgICAgICAgUzEgLS0+IEFQSQogICAgICAgIFMyIC0tPiBBUEkKICAgIGVuZAogICAgT1NbKE9iamVjdCBzdG9yYWdlKV0KICAgIEJ1aWxkW0NJIGJ1aWxkXSAtLT58aGFzaGVkIGltbXV0YWJsZSBhc3NldHN8IE9TCiAgICBVIC0tPnxhbnljYXN0IC8gRE5TfCBDRE4KICAgIENETiAtLT58c3RhdGljIGNhY2hlIG1pc3N8IE9TCiAgICBDRE4gLS0+fGR5bmFtaWMgb3Igc3RhbGUgSFRNTHwgTEI=?type=png&bgColor=FFFFFF)

Trace one request through it:

1. The request is routed by anycast/DNS to the nearest **CDN edge**.
2. If the edge holds the asset, or a fresh-enough cached HTML page, it answers immediately - the vast majority of traffic ends here, near the user, never reaching your servers.
3. On a **static miss**, the edge pulls the file from **object storage** (where the CI build published hashed, immutable assets), caches it, and serves it.
4. On a **dynamic or stale-HTML miss**, the edge goes to the **origin**: a load balancer spreads the request across a stateless, autoscaled pool of **SSR render servers**, which call the **backend APIs** for data, render the HTML, and return it - and the edge caches that result with `stale-while-revalidate`, so the next user is a hit.

The shape is the whole lesson: the edge absorbs the load, the origin only ever sees the misses, and the static build is just files. That is why a well-arranged frontend scales - most requests never touch the parts that cost you anything.

## The scale ladder

Put together, scaling a frontend is a short, ordered climb:

1. **One server serving everything** - fine until it is not.
2. **Move static assets to object storage + a CDN.** Immutable, hashed, cached at the edge. This alone offloads the vast majority of traffic.
3. **Put the CDN in front of the dynamic pages too** - edge-cache the HTML with `s-maxage` and `stale-while-revalidate`, and use ISR so most "dynamic" requests are served static-fast.
4. **Scale the render tier horizontally**, and push SSR to the edge for the pages that must render per request.

Do those in order and a frontend stops being a scaling problem long before you run out of moves. Next in this set we go behind the CDN to the part that actually computes: scaling the backend.

