studyDDIAChapter 7 · Sharding

A Timestamp Partition Key Funnels 100% of Writes Into One Shard

100,000 sensor readings, 12 key-range shards. Key the readings by timestamp and 100.0% of writes land in one shard while 11 sit idle — a self-inflicted hot spot. Prefix the key with the sensor id and the writes spread to all 12 shards (busiest just 1.024× the mean) — but a time-range query across all sensors now touches all 12 shards instead of 1.


Concept

Key-range sharding gives each shard a contiguous slice of the key space — the property that makes a range scan cheap, because adjacent keys live together. But choose a monotonically increasing partition key (a timestamp) and that same ordering turns against you: the maximum key always lives in the last shard, so a write-as-you-go workload funnels 100% of writes into one shard while the other 11 sit idle. A self-inflicted write hot spot. The book's fix is to prefix the key with something high-cardinality — the sensor id — so the write point scatters across the sensor-id range and load spreads evenly (busiest shard drops to 1.024× the mean). The documented cost lands immediately: a time-range query across all sensors, one contiguous scan under the timestamp key, now touches all 12 shards — one scan per sensor. Same 100,000 readings, same 12 range shards; the only thing that changes is which field the key sorts by.

Provenance

Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 7 — Sharding, §"Sharding by Key Range" (the sensor-network example):

The downside of key-range sharding is that it can lead to hot spots if the key [...] is a timestamp, [...] because all writes end up going to the same partition (the one for the current day) [...]. To avoid this problem [...] you could prefix each timestamp with the sensor name so that the [sharding] is first by sensor name and then by time.

The book warns that the fix has a price: "if you want to fetch the values of multiple sensors within a time range, you now need to perform a separate range query for each sensor name." This exercise makes both halves — the hot spot and its cost — into numbers you can predict against.

Prerequisites

The surprise is about how key order interacts with write order. Each line notes where it shows up.

  1. Key-range vs. hash sharding — key-range gives each shard a contiguous span of keys (cheap range scans, risky hot spots); hashing scatters keys to spread load (even writes, but no range scans). This exercise is entirely inside the key-range world; the hot spot is the price of contiguity. It shows up as the shard_by_timestamp / shard_by_sensor_key range routers.
  2. Sequential vs. random key access — a monotonic key (timestamp) writes to one ever-advancing point; a high-cardinality key (sensor id) writes to many points at once. It shows up as scenario A's single busy shard vs. scenario B's twelve.
  3. Data locality — placing related rows physically together makes reading them cheap; the cross-sensor time-range query is fast exactly when the matching keys are contiguous. It shows up as "shards touched = 1" vs. "= 12".
Where to learn the prerequisites Key-range vs. hash sharding (#1): DDIA §"Sharding by Key Range" and §"Sharding by Hash of Key" — the two schemes and the trade-off between range scans and even load. Sequential-key hot spots (#2): the AWS/Google "avoid monotonically increasing keys" guidance for DynamoDB partition keys and Bigtable/Spanner row keys — the same anti-pattern in production docs. Data locality (#3): DDIA §"Sharding by Key Range" on why keeping a range physically ordered lets a scan read it in one sweep. If only one is new, make it #2 — a sequential partition key is the entire cause of the hot spot.
Environment these numbers came from Deterministic, so the counts reproduce exactly. Captured on macOS 26.5.2 (Darwin 25.5.0), arm64, Python 3.15.0a8, standard library only — no Docker, no deps. The only randomness is sensor assignment, seeded random.Random(42). Fixed workload: 100,000 readings, 1,000 sensors, 12 range shards, timestamps strictly increasing within the current month.

Mental model

Two partition-key choices over the identical stream of 100,000 readings. The shards are range-based both times (each owns a contiguous slice); only the key differs.

Both schemes route with a range boundary lookup (integer division for time, a bisect over sensor-id boundaries for the string key). The whole thing is one script, code/key_range_hot_spot.py.

Setup

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

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


Step 1 · timestamp partition key: where do the writes land?

Predict. 100,000 readings stream in with monotonically increasing timestamps, all inside the current month, across 12 shards that each own one month of time. How many of the 12 shards receive writes, and what share does the busiest hold?

How many shards get writes, and what share does the busiest hold?
per-shard write counts: shard 0 : 0 [........................................] shard 1 : 0 [........................................] shard 2 : 0 [........................................] shard 3 : 0 [........................................] shard 4 : 0 [........................................] shard 5 : 0 [........................................] shard 6 : 0 [........................................] shard 7 : 0 [........................................] shard 8 : 0 [........................................] shard 9 : 0 [........................................] shard 10 : 0 [........................................] shard 11 : 100,000 [########################################] busiest shard holds 100,000 of 100,000 writes = 100.0% 11 of 12 shards are completely idle -- a self-inflicted write HOT SPOT.

One shard holds everything. Shard 11 owns the current month, the current month holds the maximum key, and every write carries a fresh maximum — so 100.0% of writes hit shard 11 and the other 11 shards never wake up. Buying 12 shards bought exactly 1× the throughput of a single machine.

Step 2 · sensor-prefixed key: does the load spread?

Predict. Same 100,000 readings, but the key is now f"{sensor}#{timestamp}" and the shards partition the 1,000-sensor id space. Roughly how evenly do the writes spread — and how far is the busiest shard from the mean of 8,333?

How evenly do the writes spread, and how far is the busiest from the mean?
per-shard write counts: shard 0 : 8,292 [###.....................................] shard 1 : 8,431 [###.....................................] shard 2 : 8,133 [###.....................................] shard 3 : 8,268 [###.....................................] shard 4 : 8,465 [###.....................................] shard 5 : 8,405 [###.....................................] shard 6 : 8,129 [###.....................................] shard 7 : 8,537 [###.....................................] shard 8 : 8,365 [###.....................................] shard 9 : 8,452 [###.....................................] shard 10 : 8,283 [###.....................................] shard 11 : 8,240 [###.....................................] max 8,537 min 8,129 mean 8,333 imbalance: busiest shard = 1.024x the mean (vs 12.0x under scenario A). writes now SPREAD across all 12 shards.

All 12 shards busy, none more than 2.4% above the mean. Prefixing with the sensor id moved the write point from "the single newest key" to "wherever this sensor sorts," and 1,000 sensors spread across the id range keep every shard fed. The hot spot is gone.

Step 3 · the cost: a time-range query across all sensors

Predict. Now ask a read: all readings in a 20,001-second window, across all sensors. Under the timestamp key (scheme A) and under the sensor-prefixed key (scheme B), how many of the 12 shards must the query touch?

How many shards must the cross-sensor time-range query touch under each key?
window [t0,t1] = [28,552,000 .. 28,572,000] -> 20,001 readings match scheme A (key = timestamp) : matching keys are CONTIGUOUS in time -> shards touched = 1 [11] scheme B (key = sensor#timestamp): matching keys are SCATTERED by sensor -> shards touched = 12 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] Spreading the writes cost the query 12x the shards to scan: one contiguous range scan became 12 scans (one per shard).

Scheme A touches 1 shard; scheme B touches all 12. Under the timestamp key the matching readings are contiguous — one range on one shard. Under the sensor-prefixed key they are sorted by sensor, so the same 20,001 readings are smeared across every shard: the query becomes a separate range scan per sensor. The fix for the write hot spot is the cause of the read fan-out.


What you should see

Why

Key-range shards are contiguous by construction: shard m owns keys in [boundary_m, boundary_{m+1}), and that is exactly what lets a range scan read a span of keys as one sequential sweep. The trouble is that "contiguous by key order" and "hot on sequential writes" are the same coin. A timestamp key is monotonic — every new reading's key is greater than every key written before it — so the write point is pinned to the top of the key space, and the top of the key space is, by the range-partition definition, a single shard: the last one. There is no arithmetic that spreads a stream of ever-increasing keys across contiguous ranges, because "ever-increasing" means "always in the same top range." The 100.0% is not bad luck; it is forced by the two design choices meeting.

Prefixing the key with the sensor id changes what the first sort field is. Now the key is sensor#timestamp, so the shard is chosen by sensor, and the 1,000 sensor ids — drawn across the whole id range — scatter the write point over all 12 ranges. The write hot spot dissolves because the leading bytes of the key are high-cardinality and roughly uniform, which is the general recipe: a key-range write hot spot is cured by making the key's high-order part high-cardinality. But you paid for it in the currency the range scan was denominated in. A "same time window, all sensors" query selects a horizontal slice of (sensor, time) space. Under timestamp keys that slice is one contiguous run (all keys with t ∈ [t0,t1] are adjacent) → one shard. Under sensor#timestamp keys that slice is vertical to the sort order — it crosses every sensor prefix — so its keys are spread over all 12 shards, one range scan each. The same locality you destroyed to spread the writes was the locality that made the cross-sensor scan cheap.

The boundary — the hot spot is specific to sequential keys Nothing here happens if the partition key is not monotonic. Hash the timestamp, use a random UUID, or use any key whose high-order bits are already high-cardinality, and key-range (or hash) sharding spreads the writes on its own — there is no newest shard to funnel into. That is the precise condition: the funnel exists only when successive writes carry successive keys. Equally, the read fan-out is specific to queries whose predicate cuts across the leading sort field; a query for one sensor's time range under scheme B is still one contiguous scan on one shard. The tension is not "range-sharding is bad" — it is that the field you sort by first is simultaneously the field your writes spread over and the field your range scans stay cheap over, and a timestamp cannot be both the thing that spreads writes and the thing that keeps time-range reads local. You choose which query gets the contiguity.

Go deeper

Sources: DDIA 2e, Ch. 7, §"Sharding by Key Range", §"Sharding by Hash of Key".