How Does Our CDN Cache Responses?
Our services sit behind Cloudflare. A single zone-wide Cache Rule makes edge caching fully origin-driven: Cloudflare never decides on its own what to cache — each service opts in per-response using standard HTTP caching headers.
This document explains how the rule works, how to opt an endpoint in or out, and what happens behind the edge.
The Cache Rule
One rule per Cloudflare zone:
| Setting | Value |
|---|---|
| When incoming requests match | (http.host wildcard r"*") — every host in the zone |
| Cache eligibility | Eligible for cache |
| Edge TTL | Use cache-control header if present, bypass cache if not |
| Browser TTL | Respect origin TTL |
The key part is the Edge TTL mode: a response is cached only when the origin explicitly sends a Cache-Control header that allows it. Anything else is proxied through untouched — exactly as if the rule did not exist.
Cache Rules are configured per zone. When a new domain (zone) is added, the same rule must be created there once.
The decision flow
- Request arrives at the edge. The rule marks it eligible, and Cloudflarelooks up its cache by cache key (hostname + path + query string).On a fresh entry: cf-cache-status: HIT, the origin is never contacted.
- On a miss, the request goes to the origin and Cloudflare inspects theresponse:
| Origin response | Edge behavior |
|---|---|
Cache-Control: public, max-age=172800 |
✅ cached for 172800 s |
Cache-Control: s-maxage=600, max-age=60 |
✅ edge caches for 600 s (s-maxage wins at the edge), browsers get 60 s |
Cache-Control: private, no-store, no-cache, or max-age=0 |
❌ not cached |
No Cache-Control header at all |
❌ not cached (the “bypass if not present” part) |
Response carries Set-Cookie |
❌ not cached, even if public, max-age is also present |
- Status codes. 2xx responses are cacheable; 5xx are not. Redirects and404s get Cloudflare’s short default TTLs when cacheable.
So the precise contract is: a response is edge-cached iff all three hold:
- Cache-Control is present with public + max-age > 0 (or s-maxage > 0);
- no forbidding directive (private, no-store, no-cache);
- no Set-Cookie header.
Why this is safe for application pages
Normally, your framework attaches a session cookie (Set-Cookie) to regular HTML responses, which disqualifies them from edge caching automatically — even if a middleware adds public, max-age. Personalized content can never leak into the shared cache by accident.
The flip side: if you want a page cached at the edge, you must make sure it is served without Set-Cookie and with an explicit public, max-age.
How to opt an endpoint in
From any service, in any language — send the right headers:
Cache-Control: public, max-age=172800and make sure the response has no Set-Cookie. That’s it; no Cloudflare configuration is needed.
To use different TTLs for the edge and for browsers:
# Edge: 7 days, browsers: 1 hourCache-Control: public, max-age=3600, s-maxage=604800or, Cloudflare-specific (takes precedence over Cache-Control at the edge and is invisible to other proxies):
CDN-Cache-Control: max-age=604800Cache-Control: public, max-age=3600How to opt out
Do nothing — that is the default. Responses without Cache-Control bypass the edge cache. To be explicit (e.g. for an API that must never be cached anywhere):
Cache-Control: no-storeInvalidation
While an entry lives at the edge, the origin cannot update it — lowering max-age only affects future entries. To drop content immediately:
- Single URLs: Cloudflare dashboard → Caching → Purge Cache → Custom Purge,or the API:
curl - Everything: Purge Everything (use sparingly — the origin takes the fullload until the cache re-warms).
Verifying behavior
curl -sI "https://example.com/some/url" | grep -iE "cf-cache-status|cache-control|set-cookie"cf-cache-status |
Meaning |
|---|---|
HIT |
served from the edge cache |
MISS |
cacheable, fetched from origin, now stored — repeat should be HIT |
EXPIRED |
was cached, TTL ran out, revalidated against origin |
DYNAMIC |
not eligible for caching (no allowing headers, or Set-Cookie) |
BYPASS |
origin explicitly forbade caching (no-store/private) |
Pitfalls checklist
Set-Cookiesilently disables caching. A stray cookie (session started on an asset route, a debug middleware) turns HIT into DYNAMIC. Strip cookies on cacheable routes at the web-server level if in doubt.max-agealso drives browsers. A long TTL means returning visitors keep stale content even after an edge purge. Split with s-maxage /CDN-Cache-Control when that matters.- The query string is part of the cache key. ?utm_source=… variants each get their own entry and their own origin miss.