Quick answer
A content delivery network (CDN) is a group of servers distributed across many locations that store copies of your content and serve it to visitors from whichever location is closest to them. Instead of every request travelling to your single origin server, most are answered nearby. This cuts latency, reduces the load your own infrastructure has to handle, and — because a cached response never touches your origin — absorbs a large share of attack traffic as a side effect.
01How a CDN works
When a visitor requests a file, DNS or anycast routing directs them to a nearby CDN location rather than to your server. If that location already holds a fresh copy, it is served immediately: a cache hit. If not, the CDN fetches it from your origin once, keeps a copy and serves later requests from its own storage: a cache miss followed by hits.
How long a copy is kept is controlled by the cache headers your origin sends, chiefly Cache-Control, defined in RFC 9111. A fingerprinted asset can be served from the edge for a year; an HTML page might be revalidated every minute; a logged-in dashboard should never be stored by a shared cache at all.
# Fingerprinted asset: the URL changes whenever the file does
Cache-Control: public, max-age=31536000, immutable
# Public HTML: fresh for a minute, then served stale while it refreshes
Cache-Control: public, max-age=60, stale-while-revalidate=600
# Anything personalised: never store
Cache-Control: private, no-store02What to cache, and for how long
The right lifetime depends on how a URL changes, not on its file type. Two extensions do much of the work: immutable (RFC 8246) tells caches that a response will not change during its freshness lifetime, and stale-while-revalidate (RFC 5861) lets a cache keep serving a just-expired copy while it fetches a new one in the background. Status codes have defaults of their own: a 301 or 404 sent without cache headers may still be stored, as which HTTP status codes can be cached explains.
| Content | Example | Cache-Control | Why |
|---|---|---|---|
| Fingerprinted assets | /assets/app.3f9c2b.js | public, max-age=31536000, immutable | A new build produces a new URL, so the old one never has to change |
| Unversioned images and fonts | /logo.png | public, max-age=86400 | Changes are rare and a day of staleness is acceptable |
| Public HTML | An article or product page | public, max-age=60, stale-while-revalidate=600 | Short freshness keeps it current; stale serving hides origin latency |
| Public API data | A price list in JSON | public, max-age=0, s-maxage=30 | Browsers revalidate every time while the CDN absorbs the repeats |
| Personalised pages | /account, /cart | private, no-store | A shared cache must never hand one user's page to another |
03What you actually get
- Lower latency: distance is physics, and a response from 30 km away beats one from 3,000 km
- Less origin load: cached responses never reach your server, so it can be smaller
- Traffic spikes absorbed: a viral page is served from the edge instead of melting your origin
- Lower bandwidth bills: you pay for the cache fill, not for every visitor, which also lowers the peaks a 95th percentile bill is calculated from
- Attack resilience: flood traffic hitting cached URLs is answered without your origin noticing
04The part that goes wrong: caching the wrong thing
The most damaging CDN misconfiguration is caching a personalised response and serving it to someone else. If a page containing one user's name, basket or session is stored at the edge, the next visitor may receive it. That is a data breach caused by a caching rule.
The safeguards are straightforward but must be deliberate. The HTTP caching standard, RFC 9111, already forbids a shared cache from reusing a response to a request that carried an Authorization header unless the response explicitly allows it, and common proxy caches skip responses that set cookies: nginx's documentation states that a response with Set-Cookie 'will not be cached'. Treat anything behind a login as uncacheable unless you have thought carefully about it, and mark it private or no-store so the outcome never rests on defaults.
05How to check whether a response came from cache
Most CDNs report what they did, though not always in the same header. The Age header (RFC 9111) gives the seconds since the response was fetched or revalidated at the origin, so a non-zero Age usually means a cached copy. Cache-Status (RFC 9211) is the standard form, with values such as hit or fwd=miss; many providers still use their own X-Cache header.
Request the same URL twice and compare. A second response that is still a miss points to a header that prevents caching, a cookie, or a cache key that differs on every request. The HTTP headers checker lists every response header and shows which CDN answered, without installing anything.
curl -sI https://example.com/assets/app.css | grep -iE '^(cache-control|age|cache-status|x-cache|vary|set-cookie):'
# A cached asset typically answers with something like:
cache-control: public, max-age=31536000, immutable
age: 5321
cache-status: ExampleCDN; hit06Tiered caching and cache stampedes
A naive CDN has a problem when something popular expires: every location that gets a request at that moment goes to the origin at once, and a single expiry becomes a burst of simultaneous requests. This is a cache stampede.
Two mechanisms prevent it. Request coalescing means concurrent misses for the same URL at one location produce a single origin fetch, with the others waiting for its result. Tiered caching adds an intermediate layer, so edge locations fetch from a regional cache rather than all reaching your server independently.
If you run your own cache in front of an application, the same ideas take a few lines of nginx. The configuration below follows your origin's Cache-Control headers, lets one request per missing item through to the origin while others wait (for up to proxy_cache_lock_timeout, five seconds by default), and keeps serving the stale copy while it refreshes or when the origin fails.
# http {} context
proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app:10m
max_size=1g inactive=60m use_temp_path=off;
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_cache app;
proxy_cache_lock on;
proxy_cache_use_stale updating error timeout http_502 http_503 http_504;
proxy_cache_background_update on;
add_header X-Cache-Status $upstream_cache_status always;
}
}07Cache keys and cache-busting
A cache stores each response under a key. RFC 9111 defines the key as the request method and target URI, and nginx's default key combines the scheme, the upstream host and the full request URI, query string included. The Vary header adds request headers to the key: Vary: Accept-Encoding is harmless, while Vary: User-Agent or Vary: Cookie can split one page into thousands of copies and wreck your hit ratio.
The same mechanism is how floods get past a cache. Appending a random query string makes every request a new key, so every request is a miss that reaches your origin. It is a standard technique in HTTP floods, and the defences are the usual ones: ignore or normalise query parameters a route does not use, keep uncacheable routes behind rate limits and WAF rules, and make sure the origin can refuse work cheaply.
08Why CDNs and security belong together
A CDN and a web application firewall occupy the same position: both are a reverse proxy sitting between your visitors and your origin. Running them as one system rather than two means a request is terminated once, inspected, and served or forwarded in a single hop, instead of paying the cost of two proxies in sequence.
It also makes caching part of your defence. During an attack, requests for URLs already in cache are answered at the edge and never reach your origin. Attack traffic aimed at cacheable content is, from the origin's point of view, absorbed for free.
09Troubleshooting common CDN problems
| Symptom | Likely cause | Fix |
|---|---|---|
| Visitors see the old version after a deploy | A long max-age on an unversioned URL | Fingerprint file names, or purge the changed URLs |
| The hit ratio stays low | Set-Cookie on static responses, Vary on User-Agent or Cookie, unique query strings | Stop setting cookies on assets, trim Vary, normalise query strings |
| A user sees someone else's account page | A personalised response was stored by the shared cache | Purge at once, mark the route private, no-store, and audit every cache rule |
| The CDN returns 502 | The origin refused or reset the connection, or the TLS handshake to it failed | Work through 502 Bad Gateway causes; Cloudflare reports the same failures as 52x error codes |
| The CDN returns 504 | The origin accepted the request but answered too slowly | Find the slow endpoint; see 504 Gateway Timeout fixes |
Questions
Frequently asked questions
Sources
Sources and verification
nginx cache configuration checked with nginx -t and run locally with nginx 1.22.1 to confirm a miss followed by a hit and that a response carrying Set-Cookie was not cached; the curl header check run with curl 7.88, Debian 12, October 2026.
Last updated