Somewhere on the other side of the planet is the one true copy of this page's data — the origin server. If every reader had to reach all the way back to it for every image, script and byte, the web would crawl: a request from Sydney to a server in Virginia is a 300-millisecond round trip before the server does any work, and the origin would collapse under the sheer number of identical requests. Yet the web feels instant. The reason is that almost nothing you load actually comes from the origin. It comes from a cache — a nearer, faster copy — and there is a whole ladder of them between you and the truth.
A cache is just
The same data is copied at layer after layer, each nearer to the user and faster than the last. A single page load might be satisfied at any rung of this ladder:
Every rung trades a little staleness risk for a lot of speed, and the art of caching is choosing that trade deliberately at each layer.
The web has a built-in caching protocol baked into
| Header | Set by | What it does |
|---|---|---|
Cache-Control: max-age=3600 | server | "This copy is fresh for 3600 seconds — serve it without asking." |
Cache-Control: no-store | server | "Never cache this at all" (e.g. a bank statement). |
Cache-Control: no-cache | server | "Cache it, but always revalidate before using." |
Expires: <date> | server | Older absolute-time form of an expiry (superseded by max-age). |
ETag: "v3-abc" | server | An opaque fingerprint (version tag) of the resource's current content. |
If-None-Match: "v3-abc" | client | "I have version v3-abc — only resend if it changed." |
304 Not Modified | server | "Your copy is still good" — an empty, tiny reply; no re-download. |
The lifecycle: the server sends a resource with max-age=3600 and an ETag. For
the next hour the browser serves its copy with no network. After that the copy is stale — but
stale does not mean wrong, it means "must check." So the browser sends a conditional request,
If-None-Match: "v3-abc". If the content is unchanged, the server replies
304 Not Modified — a few bytes, no body — and the browser keeps using its copy for
another hour. Only if the content genuinely changed does the server send the full new resource. This
revalidation makes "check if fresh" almost free compared to "download again."
A cache is smaller than the data it fronts — that is the whole point, it lives in fast, scarce memory. So when it fills up and a new item must go in, something has to be thrown out. The rule for choosing the victim is the eviction policy:
The score that matters is the hit ratio: the fraction of requests served from the cache rather than passed through to the origin. A hit ratio of 0.9 means the origin sees only one request in ten — a tenfold reduction in load, and most users getting the fast path. Hit ratio climbs with cache size, but with diminishing returns: once the cache holds the "hot" working set, making it bigger buys little, because the long tail of rarely-touched items would be evicted before they're ever reused anyway.
Drag the working-set slider: a workload with a small, concentrated hot set saturates its hit ratio with a tiny cache; a workload spread over a huge working set needs a far bigger cache to reach the same ratio. Right-sizing a cache is reading exactly this curve for your traffic.
Let's implement LRU from scratch and run a request stream through it. The trick is to keep items in
recency order: on every access, move the item to the "most recent" end; when full, evict from
the "least recent" end. A JavaScript Map remembers insertion order, so deleting and
re-inserting a key on access is a neat way to keep the order honest.
With capacity 2 the cache thrashes — every one-off request (c, d,
e) evicts part of the hot set, which then misses next time. Bump the capacity to 4 and the
hot items a and b stay resident, so the hit ratio jumps. Same stream, same
policy — only the size changed. That is the diminishing-returns curve above, measured.
A CDN is caching turned into planet-scale infrastructure: a company (Cloudflare, Akamai, Fastly) runs thousands of cache servers — edge or Points of Presence — in data centres in hundreds of cities. Your site's static assets (and increasingly its dynamic responses) are cached at these edges, close to wherever your users physically are. A reader in Sydney is served by an edge in Sydney; a reader in Oslo by an edge in Oslo. The 300-millisecond ocean crossing becomes a 5-millisecond hop across town.
How does a user's request reach the nearby edge instead of the distant origin? By the same steering tricks a load balancer uses: DNS hands each user the address of a close edge, or — more cleverly — the CDN uses anycast, announcing the same IP address from every edge so the internet's own routing delivers each user to the topologically nearest one. When an edge gets a request for something it hasn't cached (a miss), it fetches from the origin once, caches it, and serves everyone else from the copy. To spare the origin from thousands of edges all missing at once, CDNs use origin shielding: a designated middle tier that the edges pull through, so the origin sees a trickle of requests instead of a flood.
Here is a failure that catches everyone once. A wildly popular item is cached with a TTL, and at the instant that TTL expires, the copy vanishes from the cache. In that same instant, ten thousand concurrent requests for it all find a miss — and all ten thousand simultaneously charge the origin database to recompute the very same value. The origin, sized to handle the trickle of misses, is flattened by the stampede; it slows, requests pile up, and the site falls over precisely because something was popular. This is the thundering herd or cache stampede. The cures: let only the first requester recompute while the others wait for it (request coalescing / a lock); serve the slightly-stale value while one background worker refreshes it (stale-while-revalidate); or jitter the TTLs so a million keys don't all expire at the same second. The common theme — never let a cache miss turn into an uncoordinated mob.
Everything so far has a cost hiding in it. Every cached copy is a bet that the original hasn't changed — and sometimes you lose that bet. A copy the cache still believes is fresh, but which no longer matches the origin, is stale, and serving it means serving wrong data: an old price, a deleted post, last week's headline. Fixing this is cache invalidation — telling every copy, everywhere, "throw yours away, it's out of date" — and it is genuinely hard because the copies are many, distributed, and not all reachable at once.
There is no free lunch here; it is a
app.9f3c1.js instead of app.js; when the file changes, the hash in the name
changes, so it's a brand-new key that was never cached. The old key simply ages out. This sidesteps
invalidation entirely and is why build tools fingerprint asset filenames.
Phil Karlton's famous quip — "There are only two hard things in computer science: cache
invalidation and naming things" — is a joke that is also completely true. Caching is easy to add
and feels like free speed, which is exactly the trap: the moment you cache something, you have created
a second copy that can silently disagree with the first, and now every write to the original must
somehow reckon with every copy of it scattered across browsers, proxies, and edges worldwide. A stale
cache does not error; it confidently serves the wrong answer — an old balance, a
just-deleted comment, a price you no longer honour — and those bugs are maddening precisely because
the code "works." The discipline is to treat every cache entry's lifetime as a deliberate decision:
pick a TTL you can defend, know exactly how a change propagates (purge? version? expiry?), and never
cache data whose staleness you cannot tolerate (use no-store for the bank statement).
Caching is a promise about time, and you must decide, on purpose, how much time you can afford to be
wrong.