Book Five · War
Restoring Balance
You survived. Now find out what actually happened.
You have four realms of caching, five defenses, and an application that stayed up. You cannot see any of it.
This lesson is about seeing — and about the harder discipline of removing caching that has not earned its place.
Measure hit ratio by key prefix
A global hit ratio is not enough, because it can average away the data you need to investigate.
Global hit ratio: 85% ← looks healthy
Broken down:
asset bundles 99.8% cheap on a miss anyway
product images 99.1% cheap on a miss
product details 94% moderate
reviews 78% moderate
price 6% ← expensive, and effectively uncached
That 85% is real, and it is hiding the fact that your most expensive query has essentially no cache at all. The cheap, high-volume things inflated the average until the problem disappeared inside it.
Also, from Lesson 2: the last few percent are worth the most. Going 90% → 99% removes 90% of your remaining misses. Going 50% → 59% removes 18%. Effort spent on an already-good ratio pays more than effort spent on a bad one — provided the traffic is expensive.
Latency percentiles
A percentile tells you what a given fraction of requests experienced.
- p50 (the median) — half of requests were faster than this
- p95 — 95% were faster; the slowest 1 in 20 were worse
- p99 — 99% were faster; the slowest 1 in 100 were worse
Averages hide caching problems, because caches make most requests fast and a few requests slow — which is precisely the shape an average erases.
Average: 12ms ← "we're fine" p50: 2ms ← the hits p95: 15ms p99: 450ms ← the misses, and every user who hit one
Your p50 is your hit path. Your p99 is your miss path. If p50 is excellent and p99 is terrible, your cache is working and your misses are expensive — that is exactly the case for single-flight and refresh-ahead from Lesson 8.
At 1,000 requests per second, the slowest 1% is 10 requests every second. Pages that make many requests have more chances to encounter a slow response.
Memory and eviction rate
From Lesson 10 — the combination is diagnostic:
| Hit ratio | Eviction rate | Diagnosis |
|---|---|---|
| Low | High | Cache is too small. Items evicted before reuse. |
| Low | Low | TTLs too short, or the data genuinely isn't reused. |
| High | High | Working, but under pressure. More memory would help. |
| High | Low | Healthy. |
A low hit ratio means very different things depending on the eviction rate, and you cannot tell them apart without both numbers.
Raising a hit ratio deliberately
Four techniques, with honest costs:
1. Adjust key granularity. Caching a whole rendered page gives one entry that must be discarded when anything on it changes. Caching pieces gives more entries, each surviving independently. Finer keys usually mean a higher hit ratio and more lookups.
2. Tune TTLs. Longer TTL, higher hit ratio, more staleness. Pure trade, and the answer comes from the business, not from you. Ask: how wrong can this be?
3. Warm the cache (L2). Attacks the cold-start portion of your misses. Effective after deploys and restarts; does nothing for steady-state misses.
4. Separate hot from cold. If 5% of products get 95% of traffic, cache the hot 5% aggressively — locally, long TTL, refresh-ahead — and barely cache the rest. This usually beats treating everything uniformly, and it is how the hot-key defense in Lesson 12 is built.
And the option people forget: accept the ratio. 60% on data that must be fresh may be the correct answer. Not every number needs improving.
Debugging across four realms
A user reports a stale price. You have four caches. Which one served it?
Without instrumentation this is guesswork. With it, it is a header.
| Header | Meaning |
|---|---|
Age: 240 | This copy has been in a cache for 240 seconds |
X-Cache: HIT / MISS | Common CDN convention (non-standard) |
Cache-Status: cdn; hit, origin; miss | The standard version — reports each cache in the chain |
Age is the most useful for stale-data reports. If a user sees a wrong price and Age: 3400, you know it came from a cache with a long TTL and can identify which. If Age: 2, the problem is not staleness — it is your database or your invalidation.
For your own layers, add your own:
X-Cache-Local: HIT X-Cache-Redis: MISS X-DB-Queries: 3
Not for production users — for you, behind a debug flag, so that “which realm served this?” is a question with an answer.
Also log cache errors separately from misses. They look identical to your application (both mean “go to the database”), and they mean completely different things. A rising cache error rate is a failing cache node. A rising miss rate is a tuning problem. Conflating them hides outages.
Test what happens when the cache dies
The question from Lesson 6, and you should now answer it experimentally.
Turn the cache off in a staging environment under realistic load.
Two possible outcomes:
It gets slower but survives. Good. Your cache is a genuine optimisation.
It falls over. Then what you have is not a cache — it is an undeclared load-bearing dependency with no durability guarantees, and a cache node failure is a full outage.
If the second happens, your options are: size the database for cache-free operation (expensive), add graceful degradation (serve stale, shed non-essential load, queue writes), make the cache highly available, or accept the risk knowingly. All four are legitimate. Not knowing which one you have chosen is not.
What not to cache
The most valuable discipline in the course. Caching is not free — it costs memory, complexity, and correctness. Some things should not be cached at all.
Anything personal, in a shared cache. The cart badge. Lesson 12 attack 5.
Anything genuinely secret. Credentials, tokens, payment details. Not in a shared cache, not on disk, no-store.
Cheap queries. An indexed primary-key lookup served from the buffer pool takes 0.3ms. A Redis lookup takes 0.5ms. You made it slower and added a staleness bug. This is Lesson 7's warning, and it is extremely common.
Data that is written far more than read. Every write invalidates; almost no read benefits. Write-around (L9) and move on.
Data that must be exact. Account balances, remaining stock at the moment of purchase, anything with a legal or financial consequence for being wrong. Read the truth.
Data with no reuse. Caching depends on the same key being asked for again. A per-request unique key is never reused, and caching it is pure cost.
The honest question
For every cache in your system:
Cost is memory, code, an invalidation path, a new failure mode, a new way to be wrong, and a permanent increase in how hard your system is to reason about.
Sometimes the answer is a clear yes. Sometimes it is a layer added years ago in front of a query that got an index six months later, and nobody has checked since.
Deleting a cache that no longer earns its place is as much a use of this technique as adding one.
At the Storefront
Instrument all four realms. Then answer, for the last request:
- Which realm served it?
- How old was the copy?
- If it missed, where did the time go?
Then turn off Redis under load and watch what happens.
“A higher hit ratio is always better.”
A 99% hit ratio on data that must be fresh is not a success — it is a bug with excellent metrics. The right target comes from what the data is allowed to be wrong about, not from the number being large.