Skip to content
Jarviix
HLD9 min read

Design a Distributed Counter

Sharded counters at 1 M+ writes/sec with bounded staleness reads, used for view counts, like counts, and rate aggregations.

hldsystem-designconsistency

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.

Related reads