Caching Pitfalls - Penetration / Breakdown / Avalanche
Three classic cache failure modes are cache penetration, cache breakdown, and cache avalanche. Understanding them helps you design TTLs, key design, and fallbacks so that caching improves performance without causing outages or stampedes. This article defines each, gives examples, and lists mitigations in a table.
Overview
- Cache penetration: Requests hit keys that do not exist in the cache and often do not exist in the database either (e.g. random IDs, attack). Every such request misses the cache and hits the DB, so the DB is overloaded.
- Cache breakdown: A hot key that exists in the cache expires at the same time. A burst of requests all miss and hit the DB at once (thundering herd). One key’s expiry causes a spike.
- Cache avalanche: Many keys expire at nearly the same time (e.g. same TTL and same batch load time). When they expire together, the DB gets a huge load spike. It’s a mass version of cache breakdown.
Example
Example 1: Cache penetration — missing key
Java// Bad: request for non-existent id = 99999 always goes to DB String key = "user:99999"; User u = cache.get(key); if (u == null) { u = db.findById(99999); // always null if (u != null) cache.set(key, u, 3600); } return u; // null; next request for 99999 hits DB again
- Mitigation: Cache "absent" with a short TTL so repeated requests for the same missing key don’t hit the DB every time. Optionally use Bloom filter to reject clearly non-existent IDs before cache/DB.
Example 2: Cache breakdown — hot key expiry
Java// All requests for "product:123" miss at TTL expiry and call DB public Product getProduct(long id) { String key = "product:" + id; Product p = cache.get(key); if (p == null) { p = db.getProduct(id); // 1000 threads do this at once cache.set(key, p, 3600); // same TTL → same expiry next time } return p; }
- Mitigation: Single-flight (e.g. per-key lock or distributed lock) so only one thread loads from DB and others wait or retry cache. Add jitter to TTL (e.g. 3600 + random(0, 300)) so keys don’t all expire together.
Example 3: Cache avalanche — batch load, same TTL
Java// At 00:00 all keys were set with TTL = 86400; at next 00:00 they all expire for (Product p : products) { cache.set("product:" + p.getId(), p, 86400); // same TTL }
- Mitigation: Stagger TTLs (e.g. base TTL + random offset). Warm cache in background before expiry (refresh-ahead). Use multiple replicas or layers so not all traffic hits one node when many keys expire.
Core Mechanism / Behavior
- Penetration: No data in cache and no (or rare) data in DB. The key is "valid" from the client’s perspective (e.g. any integer id), so you must either cache the "does not exist" result or filter invalid keys.
- Breakdown: One hot key; expiry causes a spike. Concurrency control (lock, single-flight) limits DB load to one (or few) loaders; TTL jitter avoids repeated synchronized expiry.
- Avalanche: Many keys expire together. Randomizing TTL and spreading load (replicas, refresh-ahead) smooths the spike.
| Pitfall | Cause | Mitigation |
|---|---|---|
| Penetration | Queries for non-existent keys | Cache "absent" with short TTL; Bloom filter |
| Breakdown | Hot key expires, many requests miss | Per-key lock / single-flight; TTL jitter |
| Avalanche | Many keys expire at once | TTL jitter; refresh-ahead; avoid same load time |
Key Rules
- Always set a TTL; use a short TTL for "absent" placeholders to avoid penetration without storing useless data long term.
- Add jitter to TTLs (e.g. 3600 + random(0, 600)) so keys don’t all expire at the same time and reduce breakdown/avalanche.
- For hot keys, use single-flight (or distributed lock) on cache miss so only one request loads from DB and others reuse the refreshed value.
What's Next
See Cache-Aside Pattern for correct read/write flow. See Expiration Strategy - TTL Design for TTL and jitter. See Hot Key & Big Key for hot key detection and handling.