Most of this series has lived inside one architecture: user, application, Redis, database. Redis is genuinely good at keeping database load down. But there's a question worth asking that Redis can't answer: what if the request never reached Redis either? Or further still — what if it never reached the application at all?
That's HTTP caching, browser caching, CDNs, and edge caching — and they lead to one of the most useful ideas in performance engineering: the fastest request is the one you never have to process.
1. Browser caching
Start at the very edge: the user's own browser. A site's logo.png, styles.css, and app.js don't need to be downloaded on every single visit. The browser can keep a local copy, and the next request becomes a local cache hit — no network request, no CDN request, no application request, no database request. It's about as cheap as a cache hit gets.
2. How the browser knows what to cache
Cache-Control headers make the decision. Cache-Control: max-age=3600 tells the browser the response is fresh for an hour. For static content that rarely changes, something more aggressive is common: Cache-Control: public, max-age=31536000, immutable.
That raises an obvious question: if the browser holds onto a file for a year, how does it ever get a new version? Through the filename itself — app.abc123.js becomes app.def456.js when the content changes, the browser sees a different URL, and it downloads the new file without needing to be told the old one expired. This is cache busting, and it's the foundation for a technique we'll come back to.
3. Cache-Control decides where a response is allowed to live, not just for how long
This is easy to undersell. Cache-Control: public, max-age=3600 means shared caches like CDNs are allowed to store the response for everyone. Cache-Control: private, max-age=3600 means only the individual browser may keep it — a CDN sitting in between must not. Cache-Control: no-store means don't keep this anywhere, period.
This distinction is a security boundary, not just a performance knob. Getting it backwards — marking personalized data public by accident — doesn't just cause staleness. It can mean one user's cache serves another user's data.
4. What actually belongs in the browser
Good candidates: JavaScript, CSS, images, fonts, static HTML, public assets, and some genuinely public API responses — a public product catalog, public configuration, public content. Bad candidates: account information, private messages, financial details, anything personalized, anything authentication-related. The browser isn't only a performance layer — it's a data boundary, and "it's just cached" is never a reason to assume something is safe to cache.
5. Enter the CDN
If the application lives in Virginia and a user is in Tokyo, every request without a CDN makes that full round trip. With a CDN, the Tokyo user hits a nearby edge location instead, and the response comes from far closer to home. A Content Delivery Network maintains many geographically distributed edge locations — sometimes called points of presence, or PoPs — each holding copies of content that originates from one authoritative source.
The CDN cache-hit and cache-miss shape is identical to the Redis pattern from earlier in this series, just relocated: a hit returns the cached resource without touching the origin; a miss fetches from the origin, returns it, and typically caches it there for the next request.
6. The layered picture, and what it actually saves
Put browser, CDN, application, Redis, and database in one request path, and every layer that answers stops the request from going any deeper.
That's not a hypothetical ratio — it's the realistic shape of a well-cached system under real traffic. Each layer isn't competing with the others; it's catching what the layer before it missed.
7. ETags and conditional requests
Here's a different mechanism worth knowing well: the browser already has product.json, but instead of re-downloading it wholesale on every request, the server can hand back an identifier for the current version — ETag: "abc123". The next time the browser asks for that resource, it includes If-None-Match: "abc123". If nothing changed, the server replies 304 Not Modified and sends no body at all.
For a large response, this is a real saving — bandwidth, transfer time, and server-side work, all avoided on a request that still technically happened but never had to pay for the expensive part.
8. CDN and Redis solve related but different problems
Redis lives close to the application and excels at application data, sessions, computed results, and frequently accessed database records. A CDN lives close to the user and excels at images, JavaScript, CSS, video, static files, and cacheable public HTTP responses. Neither replaces the other — most real systems that need both use both, each doing the job it's actually good at.
9. Edge caching isn't only for static files anymore
The instinct is "CDN = static assets," but modern edge platforms can cache dynamic HTTP responses too. If GET /products/popular returns an identical response for a large share of users, caching that response at the edge turns 100,000 application requests into a small number of them — occasionally, on a miss, one request reaches the application, Redis, and the database, and the CDN caches the result for everyone else. This can change the entire scalability profile of an API without touching the backend at all.
10. Personalized data breaks the pattern completely
GET /account/profile cannot be cached and served identically to everyone — Alice's balance is not Bob's balance, and caching that response globally means Alice eventually sees Bob's data. Every caching decision needs to ask: is this response public, personalized, or sensitive, and can it tolerate staleness at all? Getting this wrong isn't a performance bug.
11. Cache keys and the Vary header
For shared HTTP caches, what makes two requests "the same" is genuinely more subtle than the URL alone. GET /products?category=laptops and GET /products?category=phones need separate cache entries even though they hit the same endpoint. Headers can matter too — a response might legitimately differ for Accept-Language: en versus Accept-Language: fr. The Vary header tells caches this explicitly: Vary: Accept-Language means the cache needs to keep a separate representation per language rather than assuming one response fits everyone.
12. Invalidation shows up here too — and gets solved cleanly
Part 5 called invalidation the hardest problem in caching, and it doesn't get easier once a resource is cached in a browser, three CDN regions, and who knows where else. Waiting for TTL works but is slow. Explicitly purging every CDN edge works but requires reaching every single one. There's a third option that sidesteps the problem almost entirely.
app.8f31c2.js becomes app.a91d22.js when the content changes. Nobody has to tell any cache anywhere to forget the old file — the old URL just stops being referenced, and its cached copies become permanently irrelevant rather than dangerously stale. This is one of the cleanest invalidation techniques that exists, and the same trick applies to Redis keys directly: product:v1:101 becoming product:v2:101 sidesteps needing to invalidate every old representation, the versioning technique from Part 9.
13. Stale-while-revalidate, at the HTTP layer
We covered this pattern in Part 5 and Part 7 as a stampede defense; HTTP has a first-class way to express it directly: Cache-Control: max-age=60, stale-while-revalidate=300. For the first 60 seconds the response is fresh. For the next 300 seconds after that, a cache may serve the stale value immediately while refreshing it in the background. If generating a response normally costs 500ms, the user never pays that cost directly — they get the existing value immediately while the refresh happens behind them. This only works where staleness is genuinely acceptable: product recommendations, probably fine; an account balance, not even close.
14. What edge caching costs you
None of this is free. Freshness suffers by definition — users can see slightly old data. Invalidation needs a real strategy, whether that's purging or the versioning technique above. Query parameters and headers can fragment one logical resource into many cache entries. Personalized responses must never leak into a shared cache. And debugging gets genuinely harder — a response might have passed through a browser cache, a CDN, an API gateway, the application, and Redis before you ever see it, and figuring out which layer actually served a given response becomes its own skill.
X-Cache: HIT or X-Cache: MISS headers, which most CDNs expose in some form, are what make that debugging tractable — being able to see "Browser: MISS, CDN: HIT" versus "Browser: MISS, CDN: MISS, Redis: HIT" turns a guessing game into a quick trace.
15. A practical decision framework
Before deciding where something belongs, it's worth running through a short list of questions: Is the data static, or close to it? Is it public and genuinely shared across users, or personalized? Is it application data that fits naturally in Redis? Is it hot enough to justify a local cache? Can it tolerate being briefly stale? How often does it actually change? How expensive is it to generate in the first place? The answers point toward a layer — browser, CDN, Redis, database, or in some cases, deliberately, nowhere at all.
The bigger lesson
The goal was never "cache everything, everywhere" — that's how systems become fragile and hard to reason about. The goal is the minimum work required to produce a correct response, and sometimes that minimum genuinely is "don't cache this." Caching, across this whole series, has really been one continuous idea at different distances from the user: CPU cache, memory, a local application cache, Redis, a CDN, the origin, the database — different costs, different consistency guarantees, different failure modes, same underlying question of how close a copy of the truth needs to sit to where it's actually needed.
A high-performance system doesn't necessarily need a bigger database cluster. Sometimes the biggest win is simply making sure most requests never reach the database — and sometimes, further still, making sure they never reach the application at all.
Which layer in your own stack is doing the most invisible work right now — browser cache, CDN, or something further back — and would you actually know if it stopped?
















