studyDDIAChapter 7 · Sharding

A Hash Spreads Keys Evenly, but One Hot Key Still Owns 19% of the Load

Hash 10,000 users onto 16 shards and the per-shard key counts come out even (max/mean 1.06). Count requests under a Zipf workload instead and the shard holding the celebrity user0 serves 18.81% of all traffic. Split the hot key into 100 sub-keys and its hot spot collapses to 4.73% — but max/mean only falls to 2.13, and a read of user0 now fans out to all 16 shards.


Concept

Assign 10,000 users to 16 shards with a good hash (md5(user_id) % 16) and the per-shard key counts come out nearly equal — max/mean ≈ 1.06, a textbook even spread. Now stop counting keys and start counting requests. Real traffic is Zipfian: a celebrity (user0) gets orders of magnitude more requests than a typical user. Sum load per shard instead of keys and the single shard holding user0 carries 18.81% of all requests — 3× its fair share — even though it holds ~1/16 of the keys. Uniform over keys is not uniform over load. The book's application-level fix is to split the hot key into 100 sub-keys (user0#00..user0#99) so its writes scatter across shards; do it and user0's hot spot collapses from 18.81% to 4.73%. The twist you'll measure: max/mean only falls from 3.01 to 2.13 (Zipf has many hot keys, not one), and a read of user0 must now gather all 100 sub-keys — fanning out to all 16 shards.

Provenance

Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 7 — Sharding, §"Skewed Workloads and Relieving Hot Spots":

...a hash function...can help reduce the hot spot. However...if one key is known to be very hot...a simple technique is to add a random number to the beginning or end of the key. Just a two-digit decimal random number would split the writes to the key evenly across 100 different keys, allowing those keys to be distributed to different shards.

The book is careful about the cost: "however, having split the writes across different keys, any reads now have to do additional work, as they have to read the data from all 100 keys and combine it." This exercise makes both halves — the write spreading and the read amplification — into numbers you predict.

Prerequisites

The surprise is that two different distributions live on the same shards: keys are uniform, requests are not. These help; each line notes where it shows up.

  1. Hash uniformity — a cryptographic hash (md5) maps arbitrary key strings to apparently-random buckets, so key % 16 spreads distinct keys almost evenly. It shows up in Step A as key-count max/mean ≈ 1.06.
  2. Zipf / power-law popularity — request frequency falls off as 1/rank^s (here s=1.1), so the #1 key alone can be a large fraction of all traffic. It shows up in Step B as user0 carrying 15.14% of requests by itself.
  3. Data skew vs. load skew — balancing how many keys land on each shard (data) is a different problem from balancing how many requests each shard serves (load). A hash solves the first and can leave the second wide open. It shows up as the gap between Step A (max/mean 1.06) and Step B (max/mean 3.01).
Where to learn the prerequisites Hash uniformity (#1): DDIA §"Sharding by Hash of Key"; and Python's hashlib docs — any of md5/sha give a near-uniform spread for distinct inputs. Zipf / power laws (#2): Wikipedia, "Zipf's law" — the 1/rank^s head is where all the load concentrates. Data skew vs. load skew (#3): DDIA §"Skewed Workloads and Relieving Hot Spots" — the section is about this distinction and why hashing does not close it. If only one is new, make it #3 — the whole exercise is the gap between an even key spread and a lopsided load spread on the very same shards.
Environment these numbers came from Deterministic, so the numbers reproduce exactly. Captured on macOS 26.5.2 (Darwin 25.5.0), arm64, Python 3.15.0a8, standard library only — no Docker, no deps. Stable hashing: int(hashlib.md5(s.encode()).hexdigest(), 16) % 16, not Python's built-in hash() (which is salted per-process and would not reproduce). Load computed analytically from the Zipf weights (no sampling), so there is zero Monte-Carlo noise — every shard percentage is exact every run.

Mental model

One set of 16 shards, two distributions laid on top of it.

The whole thing is one deterministic script, code/hot_key_skew.py — it counts keys, then sums Zipf load, then splits the hot key and re-sums.

Setup

cd study/ddia/ch07
python3 code/hot_key_skew.py

Do not read the output yet — make each prediction first.


Step 1 · key distribution

Predict. 10,000 users are hashed onto 16 shards by md5(user_id) % 16. Counting keys per shard, how even is it? Give the max/mean imbalance you expect.

How even is the key spread?
Step A -- 10000 keys over 16 shards, assigned by md5(user_id) % 16 per-shard KEY COUNT: shard 0 | 591 ## shard 1 | 636 ### shard 2 | 628 ### shard 3 | 616 ## shard 4 | 665 ### shard 5 | 662 ### shard 6 | 636 ### shard 7 | 599 ## shard 8 | 651 ### shard 9 | 607 ## shard 10 | 654 ### shard 11 | 603 ## shard 12 | 579 ## shard 13 | 590 ## shard 14 | 640 ### shard 15 | 643 ### min=579 max=665 mean=625.0 key-count imbalance max/mean = 1.06 (a good hash spreads keys evenly)

max/mean = 1.06 — every shard holds within 6% of the mean 625. The hash did its job: distinct keys are spread almost perfectly evenly. If you stop here you'd conclude the shards are balanced.

Step 2 · request load under a Zipf workload

Predict. Same shards, same assignment. Now requests follow a Zipf law (1/rank^1.1), with user0 the celebrity at rank 1. Sum load per shard. Will it still be ≈ 1.06? What fraction of all requests hits the single hottest shard?

Is the load still even, and how hot is the hottest shard?
Step B -- SAME shards, but requests follow Zipf (s=1.1); user0 = celebrity per-shard REQUEST LOAD (share of all requests): shard 0 | 8.04% ### shard 1 | 4.09% ## shard 2 | 5.03% ## shard 3 | 5.55% ## shard 4 | 3.29% # shard 5 | 5.02% ## shard 6 | 7.56% ### shard 7 | 2.98% # shard 8 | 8.79% #### shard 9 | 18.81% ######## shard 10 | 4.29% ## shard 11 | 4.10% ## shard 12 | 2.78% # shard 13 | 11.81% ##### shard 14 | 4.83% ## shard 15 | 3.02% # hottest is shard 9 (holds user0): 18.81% of ALL requests user0 alone is 15.14% of all requests load imbalance max/mean = 3.01 (uniform over keys is NOT uniform over load)

max/mean = 3.01, not 1.06 — the same shards, a totally different picture. Shard 9 serves 18.81% of all requests (fair share is 6.25%), almost entirely because it holds user0, who alone is 15.14% of traffic. The hash never had a chance: it balances keys, and load is glued to keys unequally.

Step 3 · split the hot key

Predict. Split user0 into 100 sub-keys user0#00..user0#99, sending 1/100 of its writes to each and hashing each independently. Two questions: what does write-load max/mean drop to — all the way to ~1.0? — and how many of the 16 shards does a single read of user0 now have to touch?

Does the load flatten to ~1.0, and how many shards does a read touch?
Step C -- split the hot key user0 into 100 sub-keys user0#00..user0#99 per-shard WRITE LOAD after splitting the hot key: shard 0 | 8.80% #### shard 1 | 4.55% ## shard 2 | 6.09% ## shard 3 | 6.16% ## shard 4 | 4.95% ## shard 5 | 6.08% ## shard 6 | 8.93% #### shard 7 | 3.74% # shard 8 | 9.25% #### shard 9 | 4.73% ## shard 10 | 5.19% ## shard 11 | 5.31% ## shard 12 | 3.38% # shard 13 | 13.32% ##### shard 14 | 5.89% ## shard 15 | 3.63% # user0's shard 9: 18.81% -> 4.73% (user0's hot spot is gone) but shard 13 -- the NEXT hot key's shard -- now leads at 13.32% write-load imbalance max/mean = 2.13 (was 3.01: better, not flat -- Zipf has MANY hot keys; you must split each one, not just the top) ...and the READ subtlety: a read of user0 must now gather all 100 sub-keys, which landed on 16 distinct shards -- so a single hot-key read now fans out to 16 of 16 shards. Write load spread; read load amplified.

The fix works on user0 — its shard falls from 18.81% to 4.73% — but max/mean only drops to 2.13, not ~1.0. Splitting one key can only fix one hot spot; the next-hottest user (on shard 13) was already there under the celebrity and now stands revealed as the tallest bar. Zipf has a whole head of hot keys, so the technique has to be applied to each one you know about. And the price of the split is read amplification: the 100 sub-keys scattered across all 16 shards, so a read of user0 now fans out to 16 of 16 shards.


What you should see

Why

A hash function is a promise about identity, not about traffic. md5(user_id) maps each distinct id to an apparently-random bucket, and with 10,000 distinct ids over 16 buckets the law of large numbers gives you ~625 per shard — max/mean 1.06. But the quantity you actually care about is requests served, and requests are attached to keys in wildly unequal amounts. Under Zipf, the weight of rank r is 1/r^1.1; normalized over 10,000 users the rank-1 key user0 carries 15.14% of all traffic by itself. Hashing scatters that 15.14% onto whichever single shard user0 lands on, and no amount of hash quality can divide one key's load across shards — a key is indivisible to the hash. So the shard holding user0 sits at 18.81% (its own baseline ~3.7% of the tail plus user0's 15.14%), while the mean is 6.25%: max/mean 3.01. Uniform over keys ≠ uniform over requests because the map from keys to load is itself skewed.

The book's fix attacks the indivisibility directly: make the one hot key into 100 keys. Appending a two-digit suffix turns user0 into user0#00..user0#99, and because each suffix hashes independently, 1/100 of user0's writes go to each — its write load is divided by ~100 and scattered. That is why shard 9 collapses from 18.81% to 4.73%. But two things temper the win. First, splitting one key fixes one hot spot: the residual max/mean of 2.13 is the next celebrity (on shard 13), which Zipf guarantees exists — the head is many keys deep, so you split each hot key you know of, not just the top. Second, the split is a write-only trick. A read must reconstruct user0 from all 100 sub-keys, and those 100 sub-keys landed on all 16 shards, so a hot-key read now fans out to every shard. You have traded read amplification for write spreading — a good trade only because writes to a celebrity vastly outnumber the (already-fanned-out) reads, and only worth doing for the handful of keys hot enough to matter.

The boundary — if the workload is uniform, the fix is pure overhead The whole problem exists because load per key is skewed. Flatten the popularity curve (every user equally likely) and Step B collapses back onto Step A: hashing already balances load, max/mean is ~1, and there is no hot key to split. Splitting keys under a uniform workload would only manufacture read fan-out for no write benefit. The technique is a targeted patch for the few keys the power law makes hot — it buys you nothing, and costs you reads, everywhere else. More generally: a hash gives you data balance for free; load balance is a separate, workload-dependent problem, and you pay for it per hot key.

Go deeper

Sources: DDIA 2e, Ch. 7, §"Skewed Workloads and Relieving Hot Spots", §"Sharding by Hash of Key".