pickuma.
Infrastructure

CDN Edge Caching Explained for Application Developers

Edge caching is not just for static assets. A properly configured CDN can serve API responses, authenticated content, and even dynamic pages from a point-of-presence 20 milliseconds from your user — if you get the Cache-Control headers right.

7 min read

Most developers interact with a CDN the way they interact with DNS: set it up once, verify it works, and forget it exists. The domain gets a CNAME record pointing at Cloudflare or Fastly or Bunny.net, static assets start loading faster, and the job is done. But edge caching is a deeper capability than asset delivery. It can serve entire API responses from a point of presence in Singapore while your origin server sleeps in Virginia, dropping latency from 250 milliseconds to 20 milliseconds for that user. The trick is knowing which responses are cacheable, how long to cache them, and how to invalidate them when the data changes.

What a CDN actually does (and does not do)

A CDN is a distributed network of servers — points of presence, or PoPs — that sit between your origin server and your users. When a user requests a URL, the nearest PoP checks whether it has a fresh copy of the response. If it does, it serves it directly. If it does not, it fetches from the origin, stores a copy according to the cache headers, and serves it to the user.

The important detail that most explanations skip: the CDN does not know what is safe to cache. It trusts your Cache-Control headers. If your origin returns Cache-Control: no-store, the CDN passes the request through every time and you get no caching benefit. If your origin returns Cache-Control: public, max-age=3600 but the response contains a user’s email address, the CDN will happily serve that email address to the next visitor who requests the same URL. The CDN is a mechanism, not a policy engine. You define the caching policy in your HTTP headers.

The other thing a CDN does not do is cache POST, PUT, PATCH, or DELETE requests. By the HTTP specification, only GET and HEAD are cacheable by default. If your application uses GET requests for search queries or filtered lists — GET /api/products?category=shoes&page=3 — those responses are cacheable, and you should set Cache-Control accordingly. If your application uses POST for everything (a GraphQL-only API, for example), your CDN will forward every request to the origin, and you are paying for a global network that is not earning its keep.

Cache-Control headers that make edge caching work

The Cache-Control header is the contract between your origin and every intermediate cache between it and the user. Three directives do the heavy lifting.

public vs private. public means the response can be stored by any cache, including shared CDN caches. private means the response is specific to one user and must not be stored by shared caches — browser caches only. If your API returns user-specific data and you set Cache-Control: public, you have a data leak. If your API returns the same JSON to every user and you set Cache-Control: private, you are paying for origin requests you do not need.

max-age. How many seconds the response is considered fresh. A response with max-age=300 can be served from cache for 5 minutes without contacting the origin. After 5 minutes, the cache marks the response as stale and fetches a fresh copy on the next request. Setting max-age too low — 5 seconds — eliminates most of the caching benefit. Setting it too high — 24 hours — means bugs and stale data live for a day after you fix them.

s-maxage. Overrides max-age specifically for shared caches (CDNs). This is useful when you want browsers to cache aggressively but the CDN to revalidate more frequently. Cache-Control: public, max-age=86400, s-maxage=60 tells browsers to cache for a day and the CDN to refresh every minute. The CDN absorbs 99% of the traffic, browsers still get fast loads, and you get 60-second freshness for the most important cache layer.

The complementary header is CDN-Cache-Control, supported by Cloudflare, Fastly, and Bunny.net. It lets you set CDN-specific caching behavior without affecting intermediary proxies or browser caches. If your origin sits behind a reverse proxy that strips or modifies Cache-Control, CDN-Cache-Control survives because it is an extension header that most proxies leave untouched.

Cache invalidation that does not break production

A cached response is a frozen snapshot of your database at some point in the past. When the database changes, the cache must change, and the options for making that happen are limited.

Purge by URL. The simplest approach: send a PURGE request to the CDN for the specific URL that changed. Cloudflare supports this via API. Fastly supports instant purge (under 150 milliseconds globally). The limitation is granularity: if a single price change affects 50 product pages, you need to purge 50 URLs, and if the CDN has a rate limit on purge requests, the purge queue can back up.

Purge by tag or surrogate key. Your origin adds a Surrogate-Key or Cache-Tag response header with one or more tags: Surrogate-Key: product-42 category-shoes. When product 42 changes, you purge by the tag product-42 and every cached response that carries that tag — the product detail page, the category listing, the search result snippet — is invalidated in one API call. This is the pattern that separates a workable invalidation strategy from a brittle one. Fastly calls them surrogate keys. Cloudflare calls them cache tags (Enterprise only). Bunny.net supports them natively.

Versioned URLs. For truly static assets like JavaScript bundles and CSS files, the invalidation strategy is to never invalidate. Every build generates a new filename with a content hash: main.a3f2b1c.js. The HTML references the latest hash. Old files live in the cache until they expire by max-age, then they are simply never requested again. No purge needed, no race condition, no cache inconsistency. For API responses, versioning is harder, but you can approximate it with a query parameter: GET /api/products?etag=<latest-db-write-timestamp>. The CDN treats different query parameters as different cache keys, so a change in the timestamp fetches a fresh response.

Measuring cache hit ratio

A CDN is only as useful as its cache hit ratio: the percentage of requests served from cache without contacting the origin. The ratio you should expect depends on your traffic pattern, but a well-configured CDN serving a reasonably cacheable workload should hit 85 to 95 percent. Below 60 percent, you are paying for a global network that is mostly forwarding requests.

Every major CDN surfaces cache hit ratio in its dashboard. The metric splits into two useful segments:

  • Byte hit ratio: what percentage of bytes were served from cache. This skews high because large static assets (images, videos, JavaScript bundles) dominate byte volume.
  • Request hit ratio: what percentage of requests were served from cache. This is the stricter metric because small, uncacheable API calls outnumber large, cacheable assets.

If your request hit ratio is low, the most common causes are:

  • Missing or overly restrictive Cache-Control headers on API responses.
  • Cookies or authorization headers that force the CDN to bypass cache (standard CDN behavior).
  • Query parameters that create unique cache keys for every request (session tokens, timestamps, random nonces).
  • A low max-age that expires responses before they are requested a second time.

Fixing a low cache hit ratio usually means adding Cache-Control to endpoints that can tolerate staleness and stripping unnecessary query parameters from cache keys. The CDN’s documentation will tell you how to configure cache key normalization for your specific provider.

FAQ

Can I cache authenticated API responses at the edge?
Yes, with caveats. Standard CDN behavior skips caching for responses that include a Set-Cookie or Authorization header, because those responses are likely user-specific. You can override this behavior with `Cache-Control: public` and by configuring the CDN to strip the Authorization header from the cache key — but you must be certain the response does not contain user-specific data, or you will leak one user's data to another. A safer pattern: cache public data at the edge and keep user-specific data at the origin. Most applications have far more public than private data, and moving the public slice to the edge still removes most of the origin load.
What is the difference between edge caching and browser caching?
Browser caching stores responses on the user's device and serves them without any network request at all — sub-millisecond latency. Edge caching stores responses at a CDN PoP, typically 10 to 50 milliseconds from the user. The key difference for developers: you cannot invalidate browser caches. Once a browser has a cached response with `max-age=86400`, you cannot reach into that browser and remove it. Edge caches can be purged. This is why `s-maxage` and `CDN-Cache-Control` are useful: set a long `max-age` for browsers (for performance), a short `s-maxage` for the CDN (for control), and purge the CDN cache when data changes.
Do I need a separate CDN if I am already on Cloudflare?
If Cloudflare is proxying your DNS (the orange cloud is on), you already have a CDN. The question is whether you are using it. Cloudflare caches static assets by default based on file extension (CSS, JS, images, fonts). It does not cache HTML or API responses unless you configure a Page Rule or Cache Rule to do so. If your origin is serving API responses without `Cache-Control` headers and you have not configured any Cloudflare caching rules, those requests are hitting your origin on every call. The CDN is there — you just have not told it what to cache.

Related tools

Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.

Related reading

See all Infrastructure articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.