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.
PitfallCauseMitigation
PenetrationQueries for non-existent keysCache "absent" with short TTL; Bloom filter
BreakdownHot key expires, many requests missPer-key lock / single-flight; TTL jitter
AvalancheMany keys expire at onceTTL 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.