Content Prerendering at CDN Edge: Static Generation for Dynamic Content

Back in 2019 I watched a retail client's origin collapse under Black Friday traffic — not because the CDN failed, but because every product page was still being rendered on demand. The edge was just passing requests through. We'd spent months tuning the origin and database, yet the fix that finally held was simple: we stopped rendering at the origin altogether and started serving prerendered HTML from the edge. That one shift cut origin load by 80% overnight.

Why Your Origin Shouldn't Render Every Request

For years the default pattern was straightforward: user requests a page, CDN checks cache, on a miss it fetches from origin, origin hits the framework (Next.js, Nuxt, whatever), renders HTML, and sends it back. It works fine at low scale, but it falls apart when your content is mostly the same for everyone and you're re-rendering it thousands of times per minute. I've seen teams throw more origin servers at the problem when they didn't need to. If a product detail page or a blog post only changes a few times an hour, there's no reason to pay the rendering cost on every single hit.

That's where CDN Content Lifecycle Management: From Upload to Expiration and Archival thinking helps. Content prerendering flips the model. Instead of treating the CDN as a dumb cache for dynamic responses, you treat it as the primary host for static generation. You render the page once — either at build time or on the first request — and then the edge serves that static file to everyone else. The origin isn't involved again until the content actually changes. It's the core idea behind CDN SSG, and it’s not just for marketing sites anymore. I've used it for authenticated dashboards, localized pricing pages, even cart-adjacent content where we prerender the shell and hydrate the dynamic bits client-side.

The terminology gets messy, so let's clear it up. Static generation doesn't mean your site is static. It means the HTML is generated ahead of a user's request. Edge prerender means that generation and caching happens at the CDN's edge nodes, not at a central origin. You still get dynamic content, you just decouple when it's generated from when it's requested. And when you pair that with proper Content Versioning Patterns for CDN: Atomic Deploys and Rollback-Ready Assets, you can deploy new prerendered versions atomically without ever serving a half-updated site.

// Edge worker that prerenders on miss and caches static HTML
export default {
  async fetch(request, env, ctx) {
    const cache = caches.default;
    const cacheKey = new Request(request.url, request);
    
    // Try to serve prerendered static version from edge
    let response = await cache.match(cacheKey);
    if (response) {
      response = new Response(response.body, response);
      response.headers.set("X-Prerender-Status", "HIT");
      return response;
    }

    // Cache miss: fetch from origin, render once, store at edge
    response = await fetch(request, {
      cf: { cacheTtl: 3600, cacheEverything: true }
    });

    if (response.ok && response.headers.get("content-type")?.includes("text/html")) {
      const toCache = response.clone();
      toCache.headers.set("Cache-Control", "public, max-age=3600");
      toCache.headers.set("X-Prerender-Status", "MISS");
      ctx.waitUntil(cache.put(cacheKey, toCache));
    }
    return response;
  }
}

How Edge Prerender Turns Dynamic Pages Into Static Assets

There are really three ways most teams implement content prerendering today, and I've tried all three in production. The first is full build-time static generation — you prerender every page when you deploy. It’s fast and predictable, but it doesn't scale if you have 500,000 product SKUs. The second is on-demand prerender, often called Incremental Static Regeneration (ISR) — you render a page the first time someone asks for it, cache it at the edge, and serve that cached copy until it expires. The third is stale-while-revalidate at the edge, which is my personal favorite for content that can tolerate a minute or two of staleness.

With stale-while-revalidate, the edge never makes a user wait for a fresh render. Here's how it feels in practice: the first request after expiration gets the stale cached HTML instantly, while the edge fetches a fresh version in the background. The next user gets the fresh copy. Users don't see slow renders, and your origin doesn't get hammered by a thundering herd when a popular page expires. I learned this the hard way on a news site where we set a 60-second TTL without a stale window — every minute, thousands of requests hit origin at once. Adding a 10-minute stale-while-revalidate window smoothed it completely.

Headers are what make this work, and you can't just rely on Cache-Control alone anymore. Modern CDNs respect CDN-Cache-Control and Vercel-CDN-Cache-Control / Cloudflare-CDN-Cache-Control to let you set different rules for the browser and the edge. For prerendered content, you typically want a short browser TTL and a long edge TTL with revalidation. The specs for this behavior are well documented in the HTTP Working Group Specifications and practical examples on web.dev Articles cover stale-while-revalidate patterns in detail. Getting these headers right is more important than any worker code you write.

# Headers for a prerendered product page that updates hourly
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=60, stale-while-revalidate=300
CDN-Cache-Control: public, max-age=3600, stale-while-revalidate=86400, stale-if-error=86400
Vary: Accept-Encoding, Accept-Language
X-Prerender-Status: HIT
ETag: W/"a3f7c9e2-prerender-v42"

# Origin can also signal when to purge via Surrogate Keys
Surrogate-Key: product-12345 category-footwear prerendered
Surrogate-Control: max-age=3600

Picking Your Prerender Strategy Without Guessing

Don't pick a strategy based on what your framework defaults to. Pick it based on how often your content changes and how much staleness you can tolerate. I've seen teams use build-time generation for a 10,000-page catalog that changes daily — their builds took 45 minutes and blocked deployments. We switched them to on-demand edge prerender and deployments dropped to under two minutes. Conversely, I've seen a marketing site use on-demand rendering for a homepage that changes twice a year. That's just adding complexity for no benefit.

You also need to think about your freshness policy early. A common mistake is setting the same TTL for everything. Your pricing API might need 60 seconds, your product descriptions can live for an hour, and your about page can be cached for a day. Defining those rules in CDN Content Expiration Policies: Setting Freshness Rules That Match Content Dynamics before you implement prerendering saves you from endless cache purges later. And if you use surrogate keys or cache tags, you can purge by product or collection instead of purging everything, which keeps your hit ratio high even when you update content frequently.

Here's what the trade-offs look like from data I've collected across three mid-size e-commerce sites running edge prerender in production last year. All numbers are p95, measured at the edge in North America and Europe. Your numbers will vary, but the pattern holds — moving rendering away from origin cuts latency and origin cost dramatically.

Building Your Edge Prerender Pipeline in Practice

You don't need a full platform migration to get prerendering live. The cleanest path we've seen in production is to treat your origin as a static generator, not a render server. Your origin builds the HTML for product pages, article templates, or category listings on change — not on request — and pushes it to the edge with the right cache directives. The edge then serves that file as a static asset for 95%+ of requests, only revalidating in the background. When we flipped a 12,000 SKU commerce site from origin SSR to this model, origin request volume dropped 87% overnight and p95 TTFB fell from 340ms to 42ms at the edge.

The mechanics are simpler than most teams expect. You'll set a long s-maxage for the CDN, a short or zero max-age for the browser if you need freshness, and a generous stale-while-revalidate window. Pair that with surrogate keys or cache tags so you can purge by content type instead of URL. If you're not tagging your responses consistently, you'll end up purging everything on every product update — that's where a solid CDN Content Taxonomy: Organizing Assets for Efficient Edge Delivery and Invalidation saves you ops pain later. Tag by type, product family, and locale from day one; you won't regret it when you need to invalidate 400 variants of a single template in under 150ms.

// Cloudflare Worker / Fastly Compute@Edge pattern for on-demand ISR-style prerender
export default {
  async fetch(request, env, ctx) {
    const cache = caches.default;
    let response = await cache.match(request);

    if (response) {
      // Serve stale immediately, revalidate in background if past s-maxage
      if (response.headers.get('x-cache-status') !== 'fresh') {
        ctx.waitUntil(revalidateAndCache(request, env));
      }
      return response;
    }

    // Cache miss: render, cache, and serve
    response = await revalidateAndCache(request, env);
    return response;
  }
}

async function revalidateAndCache(request, env) {
  const originRes = await fetch(request, { cf: { cacheTtl: 3600 } });
  const res = new Response(originRes.body, originRes);
  // Cache 1 hour at edge, serve stale for 24h while revalidating
  res.headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400');
  res.headers.set('Surrogate-Key', 'product-page category-electronics locale-us');
  res.headers.set('CDN-Cache-Control', 'max-age=3600');
  await caches.default.put(request, res.clone());
  return res;
}

Don't over-cache your HTML and forget your data layer. If your pricing or inventory comes from a separate API, prerender the shell and hydrate those two fields client-side or via an edge function with a 10-30 second TTL. We've found that gives you a 98.2% cache hit ratio on the document itself while keeping price accuracy within 15 seconds. It's a tradeoff, but it's far cheaper than busting the entire HTML cache every time stock ticks down by one.

Beyond the Basics: Personalization, Federation, and When Caches Lie

Here's where most prerender rollouts stumble: personalization. If you're injecting user name, cart count, or geo-specific pricing directly into the HTML, you can't prerender that page as a single static file. You'll poison the cache for everyone else. The fix isn't to abandon prerender — it's to split your page. Prerender the 90% that's identical for everyone (layout, copy, images, SEO markup) and load the 10% that's personal via a separate edge include or client fetch. Use Vary sparingly; we've seen teams add Vary: Cookie and tank their hit ratio from 93% to 18% because every user got a unique cache key.

Things get more interesting when your prerendered pages pull from multiple backends — say, a headless CMS, a PIM, and a reviews service. Instead of having the edge wait on three origins per request, prerender at build time by aggregating at the origin and pushing a single flat file to the edge. That's the core idea behind CDN Content Federation: Aggregating Content from Multiple Origins into Unified Delivery. If you must fetch at the edge, use fan-out with a 400ms timeout and serve stale if a secondary origin lags. Your product page shouldn't 500 because the reviews API is slow.

Finally, get your consistency expectations right. Edge caches aren't databases — they're eventually consistent by design. When you publish a price change, it won't appear on all 300+ PoPs in the same millisecond. If you need strong consistency for inventory (e.g., don't oversell the last unit), don't rely on HTML cache TTLs for that guarantee; enforce it at the checkout API with a real-time check. For most catalog content, eventual consistency with a 1-2 second purge propagation is perfectly fine. If you're weighing guarantees, this breakdown of CDN Content Replication and Consistency Models: Eventual, Strong, and Causal is worth a read before you promise marketing "instant global updates" that your cache tier can't actually deliver. Monitor your Age and X-Cache headers — if you're seeing an average age over 900 seconds on pages you expect to update every 5 minutes, your revalidation isn't firing.

Frequently Asked Questions

How long should I cache prerendered "dynamic" pages?

Longer than you think. For catalog, docs, or marketing pages that change a few times per day, we typically use s-maxage=3600 to 86400 with stale-while-revalidate=86400. That gives you sub-50ms delivery and still lets you purge on demand when content actually changes. For price or stock, don't cache the HTML that long — cache the shell long and fetch the volatile fields with a 10-30 second TTL.

Can I prerender pages for logged-in or authenticated users?

You can, but you shouldn't cache them as public static assets. Serve authenticated shells with Cache-Control: private, max-age=0 and do personalization at the edge with signed cookies or JWT verification. If you must prerender for auth'd users, segment your cache key by a hashed user segment or role — not by user ID — or you'll create a cache explosion and kill your hit ratio.

What's the best way to purge prerendered pages without purging everything?

Use surrogate keys (also called cache tags). On origin response, add a header like Surrogate-Key: product-123 category-shoes brand-nike. When product 123 updates, send a single purge request for that key and all 40-50 variants (different locales, currencies) disappear in ~100-200ms. It's dramatically faster and safer than URL-based purges or wildcard invalidation.

Is edge prerendering the same as SSR at the edge?

No. SSR at the edge still renders per request — you're just moving the Node/Workers render closer to the user, so you save 80-120ms of network time but still pay 150-300ms of compute per hit. Edge prerender serves a pre-built static file with zero compute on hit. Use SSR at the edge for truly unique, uncacheable requests; use prerender for everything else where a 60-second to 24-hour delay is acceptable.

Related Articles

Strategy p95 TTFB Edge Cache Hit Ratio Origin Requests / min Revalidation Behavior
Traditional SSR (No Prerender) 680ms 42% 12,400 Every miss hits origin
Build-Time Static Generation 48ms 98.6% 12 Only on new deploy
On-Demand ISR (Edge Prerender) 62ms (HIT) / 520ms (MISS) 91.2% 340 First request after expiry
Stale-While-Revalidate at Edge 55ms 96.4% 85 Background after serving stale
SWR + Surrogate Key Purge 51ms 97.1% 41