Database Caching at Scale · Cache Failure Modes

Hot Keys and Big Keys

One key receiving a huge share of traffic, or one value that is megabytes large — both overload a single cache node.

Scenario

Example app: a sports app caches each match in Redis Cluster (6 shards). During a cup final, one match key gets 250,000 reads per second. Separately, a "full league standings" value has grown to 6 MB.

What you see in production

One Redis shard sits at 100% CPU while the other five idle; its network is saturated and timeouts appear for every key on that shard, not just the match. Reads of the 6 MB value take 30 ms and block the single-threaded Redis event loop for other clients.

The naive code

match = json.loads(r.get(f"match:{match_id}"))          # all 250k rps → one cluster slot → one shard
standings = json.loads(r.get("league:standings:all"))   # 6 MB value on every page view

Reproduce it

Load per shard when one key takes 40% of traffic, and after adding an in-process L1 cache and splitting the key into replicas:

Hot key load per shardTry it yourself
import zlib
SHARDS, TOTAL = 6, 600_000
hot_share = 0.40

def shard(key): return zlib.crc32(key.encode()) % SHARDS

def load(hot_copies=1, l1_hit=0.0):
    per = [TOTAL * (1 - hot_share) / SHARDS] * SHARDS
    hot = TOTAL * hot_share * (1 - l1_hit)               # L1 absorbs most reads in-process
    for c in range(hot_copies):
        per[shard(f"match:final#{c}")] += hot / hot_copies
    return [round(x / 1000) for x in per]

print("naive              k-ops/s per shard:", load())
print("8 key copies       k-ops/s per shard:", load(hot_copies=8))
print("L1 cache (95% hit) k-ops/s per shard:", load(l1_hit=0.95))

Output

naive              k-ops/s per shard: [300, 60, 60, 60, 60, 60]
8 key copies       k-ops/s per shard: [120, 60, 90, 150, 90, 90]
L1 cache (95% hit) k-ops/s per shard: [72, 60, 60, 60, 60, 60]

Root cause

A cache cluster spreads keys, not requests. All reads of one key go to the shard that owns its slot, so a single hot key caps at one node's throughput. Big values amplify this: network bandwidth and Redis's single-threaded command processing are consumed per byte, and large GET/DEL operations stall other clients.

The fix

  • L1 in-process cache for the hottest keys with a short TTL (1–5 s): most reads never leave the app server.
  • Key replication: store N copies (match:final#0..7) and read a random copy; write/invalidate all copies.
  • Split big values: per-page or per-field keys (hashes), or compress; serve partial data.
  • Detect hot keys early and treat them specially (events like finals are predictable).
from cachetools import TTLCache
l1 = TTLCache(maxsize=10_000, ttl=2)

def get_match(mid, copies=8):
    if (v := l1.get(mid)) is not None:
        return v
    raw = r.get(f"match:{mid}#{random.randrange(copies)}")
    value = json.loads(raw) if raw else load_and_fill(mid, copies)
    l1[mid] = value
    return value

def load_and_fill(mid, copies):
    value = db.fetch_match(mid)
    pipe = r.pipeline()
    for c in range(copies):
        pipe.set(f"match:{mid}#{c}", json.dumps(value), ex=30)
    pipe.execute()
    return value

Trade-offs

  • L1 caches are per instance: invalidation needs pub/sub or very short TTLs (next module), and each instance may briefly disagree.
  • Key copies multiply memory and invalidation work; use them only for the few keys that need them.
  • Splitting big values means more round trips (use pipelining / MGET / HMGET).

Detect it in production

  • Per-shard CPU, ops/s and network; one shard far above the others means a hot key or big key.
  • --hotkeys / --bigkeys scans, slowlog entries for large values, client-side per-key metrics for top keys.

Key points

  • Clusters shard keys, not requests: one hot key overloads one node.
  • Use L1 caches, key replication and smaller values for the hottest data.
  • Monitor per-shard load and find hot/big keys before big events.

Practice questions

Why doesn't adding more Redis Cluster shards fix a single hot key?

Answer

All reads of a key go to the shard owning its hash slot, so its throughput is limited by one node regardless of cluster size.

How does an in-process L1 cache help with hot keys, and what's the catch?

Answer

It serves most reads from application memory so they never reach Redis. The catch is per-instance staleness: invalidations need pub/sub or very short TTLs.

Why are very large cached values a problem in Redis?

Answer

Redis processes commands on a single thread; large GETs/DELs consume network bandwidth and processing time, delaying all other clients on that node.

Open in the app to mark lessons complete, take notes and track your progress.