Quick Answer

Modern CDNs don't just cache static files forever — they use directives like stale-while-revalidate to serve a slightly-old response instantly while fetching a fresh copy in the background, and they run actual code at edge locations (Cloudflare Workers and similar platforms) to compute or personalize responses close to the user instead of only relaying pre-baked cached bytes.

The Gap Static Caching Leaves Open

A CDN's classic job — caching an image, a CSS file, or a fully static HTML page at edge locations around the world so a user in Bangalore isn't fetching bytes from a server in Virginia — is well covered ground; if you want the mechanics of edge caching and DNS anycast itself, that's already written up in our earlier post on how CDNs make a site feel faster. This post picks up exactly where that one stops being enough: what happens when the content genuinely isn't static. A product page whose stock count changes hourly, a news homepage that updates every few minutes, a logged-in dashboard that's different for every single user — none of these can just be cached forever with a long max-age, because doing so would show stale or wrong data to real people. But refusing to cache them at all throws away most of a CDN's value and sends every single request all the way back to origin.

Two ideas solve this gap without giving up on caching entirely: stale-while-revalidate, a Cache-Control directive that lets a slightly-old response go out instantly while a fresh one is fetched in the background, and edge functions, actual code running at the CDN's edge locations instead of only relaying pre-baked responses, which lets you compute or personalize a response close to the user instead of routing every dynamic request back to a single origin server.

stale-while-revalidate: Serve Old, Fetch New, In The Background

stale-while-revalidate is a Cache-Control extension defined in RFC 5861 (also documented in MDN's Cache-Control reference), and its exact syntax is a number of seconds attached as a directive alongside a normal max-age:

Cache-Control: max-age=600, stale-while-revalidate=30

Here's precisely what that header means, per the RFC's own wording: for the first 600 seconds (max-age), the response is fully fresh and gets served as-is with no extra work. Once it passes 600 seconds and becomes stale, the cache is allowed to keep serving that same stale response for up to another 30 seconds (the stale-while-revalidate window) — but the moment it does that, it's also supposed to kick off a background request to origin to fetch a fresh copy, without making the user's request wait for that fetch to finish. The next user request after the background fetch completes gets the fresh version; nobody in that stale-while-revalidate window was blocked waiting on origin at all.

The important word in the RFC's own description is non-blocking. A plain max-age cache has exactly two states from the requester's point of view: fresh (serve immediately) or expired (block this request while origin is contacted). stale-while-revalidate adds a third state — stale-but-still-usable — that avoids the latency spike an expired cache entry would otherwise cause on the very next request after expiry, which is precisely the request most likely to be a real user rather than a background job.

Walking Through a Real Request Timeline

Take the header above and trace what happens to a page across several requests over time:

  • t=0s to t=600s — every request gets the cached response instantly. It's fully fresh; origin isn't contacted at all.
  • t=615s — a request arrives after the 600-second max-age has expired, but still within the 30-second stale-while-revalidate window. The cache serves the stale response immediately (fast, no waiting) and, in the background, fires off a request to origin for a fresh copy.
  • t=616s — the background revalidation from the previous request finishes and the cache now holds a fresh copy. Any request from this point forward gets the new content, instantly, no background fetch needed until this cycle repeats.
  • t=640s (if, hypothetically, nobody had requested the page between 600s and 630s to trigger a revalidation) — the stale-while-revalidate window has now also expired, so the cache falls back to normal behavior: this request blocks while origin is contacted directly.

The practical effect is that as long as a resource gets requested reasonably often relative to its stale-while-revalidate window, real users almost never experience the latency spike of a cache miss — some request eventually "absorbs" the background revalidation cost on everyone else's behalf, invisibly.

Edge Functions: Running Actual Code Close To The User

stale-while-revalidate helps with content that changes on its own timeline, but it doesn't help with content that's genuinely different per request — a personalized recommendation, an A/B test variant, a request that needs to check a cookie before deciding what to return. For that, modern CDNs let you deploy actual JavaScript (or WASM) that runs at each edge location itself, rather than only caching what a single origin server produced. Cloudflare Workers is one of the best-known examples, and its current fetch handler shape is genuinely simple — this is the real, current API, not an approximation of an older version:

// Cloudflare Workers fetch handler - runs at the edge, close to the user
export default {
  async fetch(request, env, ctx) {
    const cacheKey = new Request(request.url, request);
    const cache = caches.default;

    let response = await cache.match(cacheKey);
    if (!response) {
      // Not cached at this edge location yet - go compute/fetch it
      response = await fetch(request); // could also call an API, run logic, etc.
      response = new Response(response.body, response);
      response.headers.set("Cache-Control", "max-age=600, stale-while-revalidate=30");
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
    }
    return response;
  },
};

What makes this meaningfully different from a traditional serverless function (an AWS Lambda behind API Gateway, for instance) is where it runs and how it starts up. A Worker executes inside a lightweight V8 isolate at whichever of the CDN's edge locations is physically closest to the requester, and isolates start in low single-digit milliseconds rather than the tens-to-hundreds of milliseconds a traditional container-based cold start can take, since there's no container or full JS runtime process to boot — the isolate is just a sandboxed slice of an already-running V8 engine. That combination — genuinely close to the user, and fast enough to start that "cold start" stops being a design constraint most requests need to worry about — is what makes it practical to run real per-request logic (auth checks, header rewriting, A/B bucketing, geolocation-based responses) at the edge instead of forwarding every single dynamic request back to one origin region.

Note what the example code is doing with the Cache API (caches.default) alongside the compute: it's not an either/or between edge functions and caching. A Worker can check its local edge cache first, serve instantly on a hit, and only run heavier logic or contact origin on a miss — combining computed responses with the same underlying stale-while-revalidate-style caching discussed above.

Cache Invalidation: Making Old Content Actually Go Away

Time-based expiry (max-age, stale-while-revalidate) handles content that goes stale predictably. It doesn't handle content that needs to change right now — a product's price just got corrected, an article had a factual error fixed, a user just changed their profile photo. For that, CDNs expose explicit purge mechanisms rather than making you wait out a TTL:

  • Purge by URL — the blunt-but-universal option: tell the CDN "this exact URL is now invalid," and the next request for it is treated as a miss and goes to origin.
  • Purge by cache tag / surrogate key — many CDNs let you tag a cached response with one or more labels when it's stored (say, product-4471 or category-shoes), so a single purge call can invalidate every cached page that happens to include that product, no matter how many different URLs rendered it — critical for something like an e-commerce catalog where one price change can appear on a product page, a category listing, and a search results page simultaneously.
  • Event-driven purge on publish — rather than relying purely on time, the publishing action itself (saving a CMS article, updating inventory) triggers an explicit purge call as part of that same operation, so the cache is invalidated the instant the underlying data changes instead of up to max-age seconds later.

Well-designed systems combine both approaches: a reasonably short TTL as a safety net that guarantees content can't stay wrong forever even if a purge call is missed, plus explicit event-driven purging for the cases where "eventually correct" isn't good enough.

Caching Content That's Different Per User

The last piece is content that's legitimately different for every visitor — a signed-in dashboard, a shopping cart, region-specific pricing. Caching this wrong (serving user A's dashboard to user B) isn't a minor bug, it's a data leak, so it deserves to be treated carefully rather than just "turning caching off" wholesale, which is the common but overly blunt response.

The standard mechanism is the Vary HTTP header, which tells a cache that responses to the same URL should be stored as separate cache entries keyed by the value of one or more other request headers — Vary: Cookie or Vary: Authorization, for example, so a logged-in user's personalized response is never served to someone with a different session cookie. In practice, most systems go further and split the page itself: cache the parts that are genuinely shared across everyone (page layout, navigation, product descriptions) aggressively, and fetch or compute the per-user parts (cart contents, "welcome back" text, personalized recommendations) separately, often via a small edge function call or a client-side fetch after the cached shell loads — which is exactly the kind of split edge functions and stale-while-revalidate together make practical: cache what's shared, compute what's not, and keep both fast.

Frequently Asked Questions

Can a CDN cache content that's different for every logged-in user? Not as one shared cache entry per URL — either the response is split so the shared parts are cached and the personalized parts are fetched or computed separately, or the Vary header is used to key the cache by a header like Cookie so each session gets its own cached entry rather than someone else's.
What's the actual difference between stale-while-revalidate and just using a short max-age? A short max-age forces every request after expiry to block while origin is contacted, while stale-while-revalidate serves the stale response immediately during its window and refreshes the cache in the background, so users don't experience the latency spike a plain expired cache entry would cause.
Do edge functions replace the origin server entirely? Not usually — edge functions are typically used for logic that benefits from running close to the user (auth checks, personalization, A/B routing, header rewriting), while heavier processing and the system of record for data still commonly lives at a central origin or database.
What happens if the origin server is down during the stale-while-revalidate window? The cache can keep serving the stale response for the duration of that window regardless, since the whole point is that it doesn't need to wait on or depend on the background revalidation succeeding to return a response to the user.
Is edge computing just another name for a CDN? No — a traditional CDN caches and relays content from an origin server, while edge computing means running actual application code at those same distributed edge locations, and modern platforms increasingly combine both in one system.