Design a Distributed Counter
Sharded counters at 1 M+ writes/sec with bounded staleness reads, used for view counts, like counts, and rate aggregations.
Intro
Counters look trivial until you need 1 M increments/sec on a single key (a viral tweet's like count, a hot YouTube view counter, an A/B-test metric). The naive `UPDATE row SET count = count + 1` doesn't scale past a few thousand QPS on a single row. The interesting design is sharded counters with periodic aggregation, plus a fast-read tier with bounded staleness.
Functional
- INCR(key, delta) — increment a counter.
- GET(key) — read the current count (bounded-staleness OK).
- RANGE(key, from, to) — windowed count (e.g. last 1 hour).
- Multi-key DECR / batch updates.
Non-functional
- Write QPS up to 1 M on a single hot key.
- Read latency p99 < 5 ms; bounded staleness ≤ 1 s.
- No lost increments under normal conditions.
- Scale linearly with shard count.
Components
Sharded counter store
N shards per key; each shard atomically incremented.
Aggregator
Sums shards into a hot read replica every ~1 s.
Read cache
Pre-summed totals served from Redis.
Time-window aggregator
Bucket into 1 m / 1 h / 1 d windows for RANGE.
Trade-offs
Sharded counter vs single CRDT (PN-Counter)
Pros
- Sharded: simple INCR; no merge logic.
- PN-Counter: convergent, multi-master.
Cons
- Sharded: needs aggregation step.
- PN-Counter: more storage per shard.
Strong consistency vs bounded staleness
Pros
- Strong: client always sees latest.
- Bounded: 100× cheaper reads.
Cons
- Strong: can't scale past few thousand QPS on one key.
Scale concerns
- Hot key (single counter at 1 M QPS).
- Aggregation lag — keep < 1 s.
- Range queries — pre-bucketize.