Notes
The signals
Listen for data that never stops: drivers sending location every four seconds, every ad click recorded, metrics from a hundred thousand servers, likes on a viral post. Do the math. A million active drivers reporting every four seconds is two hundred and fifty thousand writes a second. When the estimate lands above ten thousand writes a second, or bursts ten times the average, this pattern applies.
- every device reports every
- few seconds (drivers, IoT)
- every click, view or like
- is recorded
- logs and metrics ingestion
- chat at WhatsApp scale
- estimate shows 10k+ writes/s
- or bursts 10x the average
The naive design
The naive design writes straight into one primary database. Each commit flushes the write-ahead log and updates every index. A single relational primary handles somewhere in the low tens of thousands of small writes a second, often less with many indexes. Read replicas do not help at all, because each replica replays every write too. Under a burst, locks pile up and requests time out.
- each commit: write-ahead log
- flush + every index updated
- one relational primary: low
- tens of thousands of small
- writes/s, often less
- replicas do not help: they
- replay the same writes
- bursts → lock waits, timeouts,
- lost data if clients give up
Move 1: cheaper writes
Move one: make each write cheaper. Every index is extra work per write, so drop the ones nothing queries. Insert in batches, because one statement with five hundred rows costs far less than five hundred statements. Append new event rows instead of updating a row in place. And storage engines built on LSM trees, like Cassandra or RocksDB, turn random writes into sequential ones. Concepts episode twenty-three explains why.
- drop indexes nobody queries
- batch: 1 insert of 500 rows
- beats 500 inserts
- append, do not update in
- place (event rows)
- async commit where losing
- the last ms is acceptable
- LSM-tree stores turn random
- writes into sequential ones
- (Concepts episode 23)
Move 2: shard
Move two is the real fix: shard. Split the data by a key, like hashing the driver ID, so each shard owns a slice of the keys and takes only its share of the writes. If one shard safely handles ten thousand writes a second, two hundred fifty thousand needs about twenty-five, so plan thirty-two. Each shard keeps its own replicas for failover. Concepts episode twenty-one covers sharding in depth.
shard = hash(driver_id) % N 250,000 writes/s ÷ 10,000 per shard (safe) = 25 shards, plus headroom → plan 32 each shard has its own replicas for failover
Choosing the key
Everything rides on the shard key. It needs many distinct values so load spreads evenly, and most queries should touch only one shard. Sharding by date is the classic mistake: every write today hits the same shard. Shard by user, device or order instead, and for time series combine device and day. Use consistent hashing so adding shards moves only a fraction of the keys.
- high cardinality, even spread
- most queries need only 1 shard
- bad: date (all writes hit
- today), country, status
- good: user_id, device_id,
- order_id, (tenant, user)
- time-series: (device, day)
- resharding: consistent hashing
- (Concepts episode 22)
Move 3: buffer
Move three: put a durable log in front of the database. The ingest service appends each event to Kafka and acknowledges as soon as the log has it, a few milliseconds. Writer processes consume at their own pace, batch, and insert. When a burst arrives at ten times normal, the backlog grows instead of the database falling over. The cost: data shows up in the database seconds later. See Concepts episode twenty-eight.
- ingest appends to Kafka and
- acks once the log is durable
- one Kafka partition: tens of
- MB/s of appends
- burst of 10x? the backlog
- grows, nothing is dropped
- writers consume, batch and
- insert at a steady rate
- reads lag by seconds
Move 4: aggregate
Move four: aggregate before you write. If you only need totals, the writer keeps counts in memory and flushes every second. Nine thousand click events for one ad become a single update adding nine thousand. That is a thousand-fold reduction. Keep the raw events in the log or in object storage, so you can recompute if a writer crashes before flushing.
raw: 9,000 click events/s for ad 7 writer keeps a map in memory: counts[ad_7] += 1 every 1 s: flush UPDATE ad_7 SET n = n + 9000 9,000 writes → 1 write keep raw events in the log or object storage for replay
Hot keys
Hot keys break even a sharded design. One viral post gets fifty thousand likes a second, and they all land on one row and one shard. The fix is a sharded counter: split the counter into sixty-four sub-counters, add to a random one on each write, and sum them on read, caching the total. Fifty thousand a second becomes about eight hundred per row.
one viral post: 50k likes/s one row → lock contention split it into 64 sub-rows: likes:post9:0 ... :63 write: pick one at random INCR likes:post9:rand(64) read: sum the 64 (cache it) 50k/s ÷ 64 ≈ 800/s per row
Move 5: the right store
Move five: choose a store built for writes. Cassandra, Scylla and DynamoDB spread writes across many partitions, and the first two use log-structured storage, so they absorb hundreds of thousands of writes a second. Column stores like ClickHouse ingest events for analytics. And sometimes the right answer is Kafka plus object storage, queried later. But keep payments and inventory in a relational database, where transactions matter more than raw speed.
- append-heavy, key lookups:
- Cassandra, ScyllaDB (LSM),
- DynamoDB (auto-partitioned)
- metrics and events:
- ClickHouse, time-series DBs
- raw firehose: Kafka + object
- storage, query later
- keep money and inventory in
- a relational DB: they need
- transactions more than speed
Failure modes
Failure handling. A writer can crash halfway through a batch, so commit the Kafka offset only after the database write succeeds. That means some events will be delivered twice, so make writes idempotent, for example with the event ID as a unique key. Alert on consumer lag; if it keeps growing, add partitions and writers. And if a shard fails, the queue keeps accepting while a replica takes over.
- writer crashes mid-batch
- → commit Kafka offset only
- after the DB write
- → redelivery: make writes
- idempotent (event_id key)
- consumer lag keeps growing
- → add partitions + writers,
- alert on lag
- shard down → its replica
- promotes, queue holds writes
Trade-offs
The trade-offs. Sharding makes cross-shard joins and transactions painful, so design queries around the shard key. A queue smooths bursts but reads are seconds behind. Aggregating loses individual events unless you keep the raw stream. Sharded counters make reads add up many rows. And the fastest write stores offer weaker consistency. Say which of these your product can accept.
- sharding: cross-shard joins
- and transactions get hard
- queue: durable and smooth,
- but reads lag by seconds
- aggregation: lose detail and
- up to 1 s on a crash
- (unless raw is kept)
- sharded counters: reads sum
- N rows
- leaderless stores: weaker
- consistency
Where it shows up
Where it shows up: the ad click aggregator and top K are all about batching and stream aggregation. The likes counter is the sharded counter problem. Uber and food delivery ingest a location every few seconds per driver. Monitoring ingests millions of metrics. WhatsApp handles billions of messages a day. And the unique ID generator exists so writes do not need a central counter.
- Ad click aggregator: batching
- Likes counter: sharded counts
- Uber: location every 4 s
- Monitoring: metrics firehose
- WhatsApp: billions of msgs
- Unique ID generator: ids for
- every write without a
- central counter
- Top K: stream aggregation
Say it like this
Here is the phrasing. Writes peak at two hundred fifty thousand a second, too many for one primary. The ingest service appends to Kafka and acknowledges. Writers batch into Cassandra, sharded by driver ID so load spreads evenly. Hot counters are split into sub-counters. Every write is idempotent by event ID, and I alert on consumer lag. Each sentence answers a follow-up before it is asked.
- "Writes peak at 250k/s, too
- much for one primary. Ingest
- appends to Kafka and acks.
- Writers batch into Cassandra,
- sharded by driver_id. Hot
- counters are split into 64
- sub-counters. Writes are
- idempotent by event_id, and
- I alert on consumer lag."