A CDN can shorten the path between a reader and a reusable response, while reducing requests to the origin. Those benefits depend on routing, cache contents, and policy. A domain pointing at a CDN does not prove that an object is cached, that the geographically nearest machine serves it, or that every request avoids the origin.
Follow the request, not the diagram
A viewer reaches an edge chosen by the provider’s routing. A usable cached response can be returned there; a miss may pass through another cache tier or reach the origin. The first request to a location can therefore behave differently from the next. Content is not necessarily copied to every edge immediately when it is published.
Uncacheable traffic can still benefit from connection handling or network routing, but it does not gain the same origin offload as a cache hit. Separate connection establishment, time to first byte, and body transfer when measuring. One fast nearby test cannot establish performance for distant readers, cold locations, or a failing origin.
Define which requests can share bytes
A cache key identifies requests allowed to share a response. Paths, selected query parameters, headers, and cookies can affect this identity. Include every input that changes the selected representation, and exclude a tracking parameter only when it truly leaves the response unchanged. Excessive variation fragments the cache; missing variation can return incorrect or sensitive content.
Forwarding an input to the origin is not the same as including it in the key. CloudFront has separate cache and origin-request policies, and the latter can forward values without keying on them. If currency is forwarded but omitted from the key while the origin uses it, a cached dollar response can answer a euro request. A better hit ratio would conceal a correctness defect.
Check the effective policy
Origin Cache-Control, validators, and Vary matter, but provider rules determine their effective handling. Verify that the configured behavior preserves each required variant. CloudFront explicitly warns that a positive minimum TTL can cache responses despite origin no-cache, no-store, or private directives. For sensitive routes, inspect both origin policy and the attached distribution policy rather than relying on one header.
In standards-based shared caching, s-maxage can supply a different lifetime from the browser-facing max-age. Revalidation and freshness remain distinct: ETag can reduce transfer during validation but cannot make an incorrect cache key safe. Versioned assets need corresponding document updates, and an edge purge does not recall a browser’s independently fresh response.
When it breaks
An omitted language or currency dimension can mix public variants; omitted authorization boundaries can be worse. Conversely, putting an irrelevant unique tracking identifier in the key makes every visit cold. Validate response bodies for contrasting inputs, not only cache-hit headers. A missing Age header alone does not establish a miss, and provider cache-status fields are not interchangeable standards.
Also plan for cold starts and purges. Several edge locations can refill independently, increasing source traffic even when each is individually correct. Test whether the origin can sustain that work, and monitor both errors and origin request counts. Repeated requests from one location do not prove that all edge caches are warm or that invalidation has completed.
Check it quickly
Use Node.js and npm; this was run with Node 26.0.0 and http-cache-semantics 4.2.0. Install the pinned library, then execute the JavaScript with NODE_PATH="$cdn_lab/node_modules" node. The exercise evaluates synthetic request metadata for one stored response using a fixed clock. It makes no network requests after installation and is not an implementation or emulator of any CDN vendor.
cdn_lab=$(mktemp -d)
npm install --prefix "$cdn_lab" --ignore-scripts --no-audit --no-fund --package-lock=false http-cache-semantics@4.2.0
const assert = require("node:assert/strict");
const CachePolicy = require("http-cache-semantics");
class FixedClockPolicy extends CachePolicy {
now() { return 0; }
}
function request(currency, campaign, language = "en") {
const url = new URL("https://edge.example.invalid/prices");
url.searchParams.set("currency", currency);
url.searchParams.set("utm_source", campaign);
return {url: url.href, method: "GET", headers: {"accept-language": language}};
}
function project(req, omitCurrency = false) {
const url = new URL(req.url);
url.searchParams.delete("utm_source");
if (omitCurrency) url.searchParams.delete("currency");
return {...req, url: url.href};
}
const original = request("USD", "alpha");
const trackingChange = request("USD", "beta");
const currencyChange = request("EUR", "beta");
const languageChange = request("USD", "alpha", "zh");
const response = {
status: 200,
headers: {
"cache-control": "public, max-age=0, s-maxage=60",
"vary": "accept-language",
},
};
const body = {currency: "USD", language: "en", price: 10};
const policy = new FixedClockPolicy(project(original), response, {shared: true});
assert(policy.storable());
assert(policy.satisfiesWithoutRevalidation(project(trackingChange)));
assert(!policy.satisfiesWithoutRevalidation(project(currencyChange)));
assert(!policy.satisfiesWithoutRevalidation(project(languageChange)));
console.log("safe projection: tracking change hits; currency/language changes miss");
const privateCache = new FixedClockPolicy(project(original), response, {shared: false});
assert(!privateCache.satisfiesWithoutRevalidation(project(original)));
console.log("s-maxage=60: fresh for shared cache; max-age=0: private cache must validate");
const broken = new FixedClockPolicy(project(original, true), response, {shared: true});
assert(broken.satisfiesWithoutRevalidation(project(currencyChange, true)));
assert.notEqual(body.currency, new URL(currencyChange.url).searchParams.get("currency"));
console.log("broken projection: EUR request can reuse the stored USD body");
for (const directive of ["private, max-age=60", "no-store, max-age=60"]) {
const prohibited = new FixedClockPolicy(original, {
status: 200, headers: {"cache-control": directive},
}, {shared: true});
assert(!prohibited.storable());
}
console.log("standards-based shared policy rejects private and no-store responses");
The safe projection removes only tracking data: a changed campaign can reuse the response, while changed currency or language cannot. The language check uses Vary in this library; verify the equivalent behavior separately on your provider. The shared policy accepts the fresh s-maxage response, while the private policy requires validation under max-age zero.
The deliberately broken projection hides currency from the policy’s request URL. The library then permits reuse even though the stored body says USD and the desired request says EUR. This is incorrect metadata supplied to the policy engine, not a library defect. Finally, the standards-based engine rejects private and no-store responses for shared storage; it does not model CloudFront’s minimum-TTL override. These assertions establish policy boundaries, not global latency, routing, purge propagation, or live account isolation.
Further reading
Sources
- CloudFront: control the cache key, cache identity and hit ratio, checked 2026-09-07.
- CloudFront: cache policy settings, query dimensions and minimum-TTL warning, checked 2026-09-07.
- CloudFront: origin request policies, forwarded inputs outside the key, checked 2026-09-07.
- RFC 9111, shared caching and response reuse, checked 2026-09-07.
- http-cache-semantics 4.2.0, request matching, shared policies, and storage checks, checked 2026-09-07.