CDNs & Caching cheat sheet
A scannable CDNs & Caching reference: 32 short snippets across 13 topics, each linking back to the lesson it came from.
At a glance
| Topic | What it covers | |
|---|---|---|
| How a CDN serves your content | A CDN is a network of reverse proxies spread across many locations. DNS or anycast routing sends each visitor to the | lesson |
| Cache headers and cache keys | Provider dashboards often override these headers with their own TTL settings. Find out which one wins before debugging | lesson |
| Invalidation, purging and what not to cache | There are three ways to invalidate: purge by URL, purge by tag or surrogate key, and purge everything. Purging one URL | lesson |
| Choosing a CDN and connecting an origin | The Host header is where most first deployments fail. The CDN forwards a Host value, the origin's virtual host does not | lesson |
| Cache hierarchy: browser, edge and origin caches | How the three cache layers interact, why a browser hit still helps, the revalidation flow, proxy caches, and designing | lesson |
| Static asset optimisation: compression and modern images | Brotli and gzip negotiation, minification and bundling decisions, WebP and AVIF with fallbacks, responsive image | lesson |
| Image CDNs and on-the-fly transformation | Origin-based versus URL-based transformation, resize and quality parameters, format negotiation, caching generated | lesson |
| Signed URLs, token authentication and hotlink protection | A signed request carries a signature computed from a shared secret and some part of the request - usually the path, an | lesson |
| Security at the edge: WAF, DDoS and bot management | Managed and custom WAF rules, rate limiting at the edge, how DDoS absorption works, bot scoring and challenges, and | lesson |
| Video and large file delivery | Range requests and seeking, segment-based streaming, adaptive bitrate basics, how large files are cached and evicted | lesson |
| Measuring CDN performance and debugging cache issues | Reading cache status headers, hit ratio and origin offload, TTFB and real user monitoring, and a step-by-step workflow | lesson |
| Multi-CDN, failover and cost optimisation | A second CDN doubles the operational surface: two configurations, two log formats, two purge APIs and two sets of rules | lesson |
| Edge architecture: personalisation and A/B testing at the edge | A page cannot be both cached for everyone and personalised for one person. The solution is structural: cache the parts | lesson |
Quick snippets
How a CDN serves your content
Edge locations and the origin
# which layer answered, and was it a hit?
curl -sI https://example.com/assets/app.js | grep -i -E "cache-control|age|etag|x-cache|cf-cache-status"
# cf-cache-status: HIT | MISS | EXPIRED | REVALIDATED | DYNAMIC
# x-cache: Hit from cloudfront / Miss from cloudfront
# age: seconds this copy has been in the cache
What a CDN also gives you
# origin: tell the edge how long it may keep the response
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
location / {
add_header Cache-Control "public, max-age=0, s-maxage=3600, stale-while-revalidate=60";
}
# and make sure a wrong host header cannot poison the cache
if ($host != "example.com") { return 444; }Full lesson: How a CDN serves your content →
Cache headers and cache keys
The headers that matter at the edge
# an asset whose name changes with its content
Cache-Control: public, max-age=31536000, immutable
# HTML: browsers revalidate, the edge holds a copy for an hour
Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=60
# anything user-specific
Cache-Control: private, no-store
# API data that may be briefly stale under load
Cache-Control: public, max-age=30, s-maxage=300, stale-while-revalidate=120
What makes up a cache key
Vary: Accept-Encoding, Accept-Language
Cache-Key: host + path + country + normalized-query
# in the CDN configuration, the equivalent rules look like:
# ignore query string params: utm_*, gclid, fbclid, msclkid
# key on: path, country (only for /pricing/*)
# never key on: cookie _session for /assets/*
Verifying from the outside
for i in 1 2; do
curl -sI "https://example.com/?utm_source=test" | grep -i -E "^(age|cache-control|x-cache)"
sleep 2
done
# strip the tracking parameter and confirm the same cached object
curl -sI "https://example.com/" | grep -i -E "^(age|x-cache)"Full lesson: Cache headers and cache keys →
Invalidation, purging and what not to cache
Getting rid of a stale copy
# purge one URL (provider-specific API shape)
curl -X POST "https://api.cdn.example/purge" \
-H "Authorization: Bearer $CDN_TOKEN" \
-d '{"urls":["https://example.com/assets/app.js"]}'
# tag the response at the origin, then purge by tag after a content update
# Surrogate-Key: article-1042 list-home
curl -X POST "https://api.cdn.example/purge" \
-H "Authorization: Bearer $CDN_TOKEN" \
-d '{"tags":["article-1042"]}'
What must not be cached
# origin: be explicit rather than relying on defaults
location /account/ {
add_header Cache-Control "private, no-store" always;
add_header X-Robots-Tag "noindex" always;
}
# edge: strip cookies from cacheable assets so they are not keyed on them
location /assets/ {
proxy_hide_header Set-Cookie;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}Full lesson: Invalidation, purging and what not to cache →
Choosing a CDN and connecting an origin
Connecting the origin
# verify the CDN is in front and the origin is not exposed
curl -sI https://cdn-stage.example.com/ | grep -iE "server|via|x-cache|cf-cache-status|age"
# verify the origin responds correctly when addressed directly
curl -sI https://origin.example.com/ -H "Host: cdn-stage.example.com" | head -5
# confirm which certificate the edge presents
echo | openssl s_client -connect cdn-stage.example.com:443 -servername cdn-stage.example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
Validating the first deployment
# first request (miss) and second request (hit) on the same asset
curl -sI https://cdn-stage.example.com/static/app.js | grep -iE "age|cache-control|x-cache"
curl -sI https://cdn-stage.example.com/static/app.js | grep -iE "age|cache-control|x-cache"
# a 404 must not be cached as a success
curl -sI https://cdn-stage.example.com/does-not-exist | head -3Full lesson: Choosing a CDN and connecting an origin →
Cache hierarchy: browser, edge and origin caches
Three layers, one request
What a request looks like at each step
1. browser has a fresh copy -> no request leaves the device
2. browser copy is stale -> conditional request with If-None-Match
or If-Modified-Since
3. edge has a fresh copy -> 200 from the edge, Age: n
4. edge copy is stale -> revalidate with the shield or origin
5. shield has a fresh copy -> 200 from the shield
6. nothing has it -> origin renders, everyone stores it
Revalidation and stale serving
# HTML that must be current but should not block the user
Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=86400, stale-if-error=3600
# a fingerprinted asset that will never change
Cache-Control: public, max-age=31536000, immutable
# an API response that is safe for a minute at the edge, never in the browser
Cache-Control: private, no-store, s-maxage=60
# an authenticated response that must never be shared
Cache-Control: private, no-store
Vary: Authorization, Cookie
Revalidation and stale serving
# see what a second request looks like
curl -sI https://example.com/ | grep -iE "cache-control|age|etag|vary"
curl -sI https://example.com/ | grep -iE "cache-control|age|etag|vary"
# force a revalidation and watch for a 304
curl -sI -H 'If-None-Match: "<etag-from-above>"' https://example.com/ | head -3Full lesson: Cache hierarchy: browser, edge and origin caches →
Static asset optimisation: compression and modern images
Compression
# Brotli when the client offers it, gzip as the fallback
brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;
gzip on;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_vary on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
Compression
# what is actually being served, and how large it really is
curl -sI -H "Accept-Encoding: br, gzip" https://example.com/static/app.js \
| grep -iE "content-encoding|content-length|vary|cache-control"
# measure the transfer size, not the decoded length
curl -s -o /dev/null -w "encoded bytes: %{size_download}\n" \
-H "Accept-Encoding: br" https://example.com/static/app.js
Modern images
<!-- modern formats with an explicit fallback -->
<picture>
<source type="image/avif" srcset="/img/hero-800.avif 800w, /img/hero-1600.avif 1600w"
sizes="(max-width: 600px) 100vw, 50vw" />
<source type="image/webp" srcset="/img/hero-800.webp 800w, /img/hero-1600.webp 1600w"
sizes="(max-width: 600px) 100vw, 50vw" />
<img src="/img/hero-800.jpg" width="1600" height="900" loading="lazy"
decoding="async" alt="Dashboard overview" />
</picture>Full lesson: Static asset optimisation: compression and modern images →
Image CDNs and on-the-fly transformation
Two ways to transform
<!-- URL-based transformation: the variant is described in the URL -->
<img src="https://cdn.example.com/img/hero.jpg?w=800&q=75&fm=webp"
srcset="https://cdn.example.com/img/hero.jpg?w=400&q=75&fm=webp 400w,
https://cdn.example.com/img/hero.jpg?w=800&q=75&fm=webp 800w,
https://cdn.example.com/img/hero.jpg?w=1600&q=75&fm=webp 1600w"
sizes="(max-width: 600px) 100vw, 60vw"
width="1600" height="900" alt="Order dashboard" />
Caching generated variants
# a transformed variant is deterministic, so it can be cached hard
Cache-Control: public, max-age=31536000, immutable
# the transformation response must vary on what actually changes it
Vary: Accept
# and a limit on the number of generated variants avoids runaway cost
# e.g. allow w in {200,400,800,1200,1600}, q fixed at 75
Caching generated variants
# is a variant cached after the first request?
curl -sI "https://cdn.example.com/img/hero.jpg?w=800&q=75" | grep -iE "age|cache-control|content-type"
curl -sI "https://cdn.example.com/img/hero.jpg?w=800&q=75" | grep -iE "age|cache-control|content-type"
# does the CDN vary by Accept?
curl -sI -H "Accept: image/webp" "https://cdn.example.com/img/hero.jpg?w=800" \
| grep -iE "content-type|vary"Full lesson: Image CDNs and on-the-fly transformation →
Signed URLs, token authentication and hotlink protection
Signing without destroying the cache
The cache-key problem
URL /v/a.mp4?expires=1700000000&sig=abc -> one cached object
URL /v/a.mp4?expires=1700000060&sig=def -> a different cached object
Sixty seconds later the same file is cached again, and the hit ratio collapses.
Fix: put the authorisation in a cookie and keep the URL stable,
or configure the CDN to exclude the signature parameters from the cache key.
Excluding them makes the object shared, so verify the token on every request.
Hotlinking and what signing cannot do
Referrer-based hotlink protection
Allow when the Referer is one of your own hostnames, or absent.
Block when it is another site.
Reality check
a Referer can be spoofed by a client, so this stops casual embedding only
an empty Referer must be allowed, or users with strict privacy settings break
search engines and link previews send no Referer - check before enablingFull lesson: Signed URLs, token authentication and hotlink protection →
Security at the edge: WAF, DDoS and bot management
WAF rules that help
A rollout order that does not break the site
1. enable managed rules in log-only mode for a week
2. read the log: what would have been blocked, and was any of it real?
3. enable the rules with no false positives, in block mode
4. add rate limits on login, signup, search and any expensive endpoint
5. add a challenge for low bot scores on those endpoints only
6. keep an allow list for your own monitors and your payment provider's
webhooks - a blocked webhook looks like a payment failure
DDoS absorption
Origin protection that survives an attack
allow only the CDN address ranges, not the whole internet
require a shared secret header that the CDN adds and the origin verifies
set a low connection limit per source at the firewall
cache aggressively at the edge so a flood of reads never reaches the origin
alarm on origin request rate, not only on error rate
Tuning without collateral damage
# a crawler must be verified by reverse DNS, never by user agent string
dig -x 66.249.66.1 +short # reverse lookup the claimed crawler address
dig +short googlebot.example.com # forward confirm it resolves back to the same range
# and a synthetic check that a real user path still works after a rule change
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/login
curl -s -o /dev/null -w "%{http_code}\n" -H "User-Agent: $UA" https://example.com/Full lesson: Security at the edge: WAF, DDoS and bot management →
Video and large file delivery
Range requests
# the client asks for a byte range
curl -sI -H "Range: bytes=0-1023" https://cdn.example.com/v/talk.mp4 \
| grep -iE "content-range|accept-ranges|content-length|age"
# a 206 response means the range was honoured
# a 200 with the full length means the server ignored the range
Range requests
Response headers for a seekable, cacheable file
Accept-Ranges: bytes
Content-Length: 52428800
Cache-Control: public, max-age=31536000, immutable
Content-Type: video/mp4
The filename should contain a hash. A 50 MB file with a stable URL cannot be
cached for a year, because you can never change it.
Segmented streaming
Adaptive bitrate in one paragraph
The video is encoded several times at different resolutions and bitrates and
cut into short segments of two to six seconds. The playlist lists the renditions.
The player measures throughput and buffer level and switches rendition between
segments, so a slow connection drops to a lower quality instead of stalling.
Cache consequences
many small segment files, all immutable and cacheable forever
playlists that change and need a short lifetime
the cost is dominated by request count, not by file sizeFull lesson: Video and large file delivery →
Measuring CDN performance and debugging cache issues
Reading the response
# one command that shows the whole story
curl -sI https://example.com/static/app.js \
| grep -iE "age|cache-control|etag|vary|via|server|x-cache|content-encoding"
# and a loop that shows a miss followed by a hit
for i in 1 2 3; do
curl -sI https://example.com/static/app.js | grep -iE "^age|x-cache"
sleep 1
done
A miss debugging workflow
# does a stray cookie header break caching?
curl -sI https://example.com/static/app.js | grep -i "set-cookie"
# is the cache key split by a query parameter?
curl -sI "https://example.com/static/app.js?utm_source=news" | grep -iE "age|x-cache"
curl -sI "https://example.com/static/app.js" | grep -iE "age|x-cache"
# and from another location, expecting an independent cache
curl -sI https://example.com/static/app.js -H "Host: example.com" --resolve example.com:443:203.0.113.9 \
| grep -iE "age|x-cache"Full lesson: Measuring CDN performance and debugging cache issues →
Multi-CDN, failover and cost optimisation
Steering traffic
DNS steering that actually fails over
primary example.com -> primary CDN CNAME, TTL 60
check synthetic request every 30 s, from several regions
threshold three consecutive failures before switching
switch replace the answer to the secondary CNAME
verify confirm the new answer from two resolvers
switch back only after the primary has been stable for 30 minutes
Remember: a client that resolved just before the switch keeps using the
old answer for the full TTL, so plan for a minute of mixed traffic.
Cost control and log unification
A single log schema for both providers
time, cdn, pop, cache_status, url, status, bytes, ttfb_ms,
client_country, user_agent_family, referer_host
With that, you can answer:
what share of requests does each provider serve?
what is the hit ratio for static assets per provider?
what is the cost per GB after normalising the log volume?
which provider is slower in the region that matters?Full lesson: Multi-CDN, failover and cost optimisation →
Edge architecture: personalisation and A/B testing at the edge
Cached shell plus fragments
<!-- the shell is cached at the edge for everyone -->
<main>
<h1>Your recommendations</h1>
<!-- the fragment is fetched separately, with the user's credentials -->
<div id="recs"
hx-get="/api/recommendations"
hx-trigger="load"
hx-swap="innerHTML">
<p class="skeleton">Loading recommendations...</p>
</div>
</main>
Cached shell plus fragments
# the shell: cacheable, shared, fast
Cache-Control: public, s-maxage=300, stale-while-revalidate=86400
# the fragment: never shared, short
Cache-Control: private, no-storeFull lesson: Edge architecture: personalisation and A/B testing at the edge →
FAQ
Is this CDNs & Caching cheat sheet free to use?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
SEO Basics Domains & DNS Web Hosting Accessibility
Last refreshed 2026-09-27.