Skip to main content
How Caching Workslesson 6 of 6 · 3 min read

How Caches Fail

Four ways a cache fails

Four problems arrive once real traffic arrives. Interviewers ask about these because they separate people who have run a cache from people who have read about one.

Stale copies you have already met. Your copy disagrees with the database, and the time limit is how long you have agreed to live with that. Choose it as a product question, not a technical one. A stock count might need thirty seconds. A product description can sit for an hour. An account balance probably should not be cached at all.

Full memory comes next. A cache has a fixed size, so when it fills, something has to go, and the rule for choosing is called an eviction policy. LRU throws out whatever has gone longest without being read, and it is the sensible default because things you just used tend to get used again. LFU throws out whatever is read least often, which suits a catalogue with a stable set of favourites. Note that a time limit is not an eviction policy. It bounds how stale a copy can get and happens to free memory as a side effect, but it makes no choice about what to drop when you are full.

The third one is the dangerous one, because it takes systems down. When a busy key expires, every request that wanted it misses at the same instant and goes to the database together. One query becomes three thousand. This is called a stampede, and the classic version is a homepage that expires at exactly midnight. Let one request rebuild the key while the others wait for it, and add a few random seconds to your time limits so keys written together do not all expire together.

The fourth is the opposite problem. A hot key is one that gets far more traffic than any other, and whichever cache server holds it saturates while the rest sit idle. Splitting your data across more servers does not help, because a single key cannot be split. Copy that one value under several keys and read one at random, or keep it in each server's own memory.

the shape of it
RequestRequestRequestRediskey just expiredDatabase3,000 at oncemissmissmissall together
step 1 of 2
One key expires and every waiting request misses at the same instant. The database gets the load the cache was absorbing.
stopping a stampede
Java
Product getProduct(long id) {
  String key = "product:" + id;
  String copy = redis.get(key);
  if (copy != null) return Product.fromJson(copy);

  // Only one caller wins this and rebuilds. Everyone else waits
  // a moment and reads again, rather than all hitting the database.
  if (redis.setnx(key + ":lock", "1", 10)) {
    try {
      Product p = db.queryOne("SELECT ... WHERE id = ?", id);
      // Random seconds on the end, so keys stored together do not
      // expire together and cause this all over again.
      int ttl = 300 + new Random().nextInt(60);
      redis.setex(key, ttl, p.toJson());
      return p;
    } finally {
      redis.del(key + ":lock");
    }
  }

  Thread.sleep(50);
  return getProduct(id);
}

Worked example

The deploy that emptied Arjun's cache was a stampede in its purest form. Three thousand requests a second, every one a miss, all landing on the database inside the same second.

The rebuild lock changed the next one completely. When a migration flushed the cache again, his database saw about 200 queries a second instead of 3,000, because for each key one request did the work and the rest waited fifty milliseconds and found it waiting for them. His database peaked at 31 percent that time, against 100 the first time.

How Caching Works: wrapping up

In the real world

  • 01Facebook runs Memcached in front of MySQL at enormous scale, and published a paper on what breaks there, including stampedes and the leases they added to stop them.
  • 02Twitter keeps timelines in Redis rather than rebuilding them per request, which is why a home timeline loads in milliseconds instead of querying everyone you follow.
  • 03Netflix built EVCache to hold personalisation data close to the services that need it, rather than paying a round trip between regions on every request.
  • 04Stack Overflow serves much of its traffic from each web server's own memory, on the grounds that the fastest network call is the one you never make.
  • 05Cloudflare and Fastly are caches at global scale: a miss at the edge is fetched from your servers once, then served locally to everyone else nearby.

Questions people ask

Should I cache everything?

No, and saying so counts in your favour. A cache earns its complexity when data is read far more often than it changes and costs something to produce. A well indexed query against a table your database already holds in memory is fast, and a cache in front of it adds a staleness problem and a new way to fail for no gain.

Why delete the key on a write instead of updating it?

Because a delete cannot arrive in the wrong order. If two edits race, one order in the database and another in the cache leaves the cache holding the loser until it expires. A delete has no value left to be stale, so the next reader rebuilds from the source. It costs one extra read on something that was about to be read anyway.

What happens if Redis goes down?

Every read misses and falls through to the database, so your system stays up and gets slow. Whether it survives depends entirely on whether the database can absorb the traffic the cache was absorbing, which is worth testing before you find out during an incident. A cold cache after a restart is dangerous for exactly the same reason.

How do I choose the time limit?

Work backwards from how wrong the data is allowed to be, because the limit is the longest anyone can see an old value. Thirty seconds for a stock count, an hour for a description, and probably no cache at all for an account balance. Then add a few random seconds so keys stored together do not all expire at the same moment.

Quick review

A cache is a copy of data kept somewhere faster, which you are allowed to lose because the source still has it
Hit and miss:
the fraction that hit is the hit rate, and 95 percent means the database sees one read in twenty
Four places a cache can live:
the client, a CDN edge, in-process memory, and a shared external store like Redis
Cache-aside is the default read pattern:
check the cache, fall back to the database, write the answer back
On a write, update the database first and then delete the key. Deleting cannot be applied out of order
The two costs are memory, which forces eviction, and staleness, which the TTL bounds
Four failures to expect:
staleness, eviction under memory pressure, stampedes on expiry, and hot keys
the trade-off

A cache buys a large drop in latency and database load, and pays for it in memory plus a window where the copy disagrees with the source. It also becomes a new thing that can fail: when it does, every read falls through, so the database has to survive the traffic the cache was absorbing.

in the room

Reach for a cache once you can name the read bottleneck in numbers. Establish the problem first, then place the cache, then say what it breaks.