Why architecture matters here

The architecture matters because contention on a single mutable value does not scale, and no amount of faster hardware fixes it. A row lock, an optimistic compare-and-swap, or a cache line ping-ponging between CPU cores all have the same shape: concurrent writers to one location must take turns. When ten thousand fans like a post in the same second, a single-row counter forces those ten thousand increments through a queue one at a time, and the p99 latency of a like balloons while the database burns CPU on lock management and retry storms. The counter is not slow because the addition is expensive — an add is nanoseconds — it is slow because the coordination around the shared value is expensive. Sharding removes the coordination by removing the sharing.

The second forcing function is that counters are often the load-bearing number in a product: view counts, credit balances, inventory levels, rate-limit tallies, quota consumption. These want both high write throughput and correctness, and the two pull apart. A naive fix — batch increments in memory and flush occasionally — buys throughput but risks losing writes on a crash; a strict transactional counter is correct but serializes. The distributed-counter architecture lets you place yourself deliberately on that spectrum: exact-but-sharded for money, approximate for vanity metrics, eventually-consistent rollups for dashboards, each with an explicit accuracy and durability contract rather than an accidental one.

The third reason is that the read pattern shapes the whole design. If reads are rare relative to writes — the usual case for a hot counter — summing N shards on demand is fine, and you can push N high to kill write contention. If reads are also hot (a leaderboard refreshed constantly), the fan-out sum becomes its own bottleneck and you need a cached rollup that a background job refreshes. Getting the read/write ratio right up front determines whether you shard for write throughput, cache for read throughput, or both — and choosing wrong means either contended writes or a read that fans out across hundreds of rows on every page load.

There is a subtler point that only shows up under sustained load: the number of shards is a tuning knob with real cost on both ends. Too few shards and writers still collide; too many and every read pays to touch a long tail of shards that are mostly zero, while the store fills with sparse rows. The right N is roughly the peak concurrent write parallelism you need to absorb — the number of cores, threads, or nodes hammering the counter at once — and it can differ per counter, which is why mature systems make shard count a per-counter property rather than a global constant baked into the schema.

Advertisement

The architecture: every piece explained

Top row: the write path. Writers are the many concurrent sources of increments — application threads, request handlers, event consumers. The shard router maps each increment to one of N shards; the mapping can be a hash of the writer id (giving a writer affinity to a shard so it never contends with itself), simple round-robin, or a random pick. The counter shards are N independent sub-counters — separate rows, keys, or partitions — each of which absorbs a slice of the write traffic with no coordination against the others. Local aggregation is an optional but powerful layer: each writer or node batches many in-memory increments and flushes their sum to its shard periodically, so a million increments become a few thousand atomic adds against the store.

Middle row: durability and reads. The durable store persists each shard, applying increments as atomic server-side adds (UPDATE ... SET c = c + delta or an atomic counter type) so a flush never needs a read-modify-write round trip. The read path produces the total either by summing all N shards on demand or by serving a precomputed value. Approximate mode handles a different question — counting distinct things (unique visitors) — with probabilistic structures like HyperLogLog or count-min sketch that answer with bounded error in tiny fixed space, because an exact distinct count does not shard the same way a sum does. Reconciliation periodically computes an authoritative total (an exact recount or a checksum) to detect and correct drift.

Bottom rows: making reads cheap and writes safe. The rollup / materialized total caches the sum so hot reads do not fan out across N shards every time; a background job refreshes it on an interval or on a change threshold, trading a bounded staleness for a constant-cost read. Idempotency and dedup guard correctness: when an increment can be retried (a client resend, an at-least-once event delivery), keying each logical increment by a unique event id and recording applied ids stops the same like from being counted twice. The ops strip names the signals that keep the counter honest: write rate, shard skew (are some shards far hotter than others?), read latency, and measured drift between the running total and a periodic exact recount.

Distributed counter — split one hot number into many shards, sum on readspread writes across counters to kill contention; trade a little read cost for enormous write throughputWritersmany concurrent incrementsShard routerhash / round-robin to N shardsCounter shardsN independent sub-countersLocal aggregationbatch + flush per shardDurable storeatomic add per shard rowRead pathsum(shards) or cached totalApproximate modeHLL / count-min for cardinalityReconciliationperiodic exact recountRollup / materialized totalcache the sum, refresh on intervalIdempotency + dedupevent-id keys guard double countsOps — write rate + shard skew + read latency + drift vs exact recountpersistrouteaggregateflushrollupsumreconcileoperateoperate
Distributed counter: writers are routed across N independent shards that aggregate locally and persist with atomic adds, while reads sum the shards or serve a cached rollup, with reconciliation and idempotency guarding accuracy.
Advertisement

End-to-end flow

Follow a viral post's like counter through the architecture. The post starts cold: one row, or a handful of shards, absorbing a trickle of likes. Traffic spikes as the post trends — thousands of likes per second now converge. The shard router spreads them across, say, 50 shards, so instead of 50,000 increments serializing on one row, each shard sees roughly 1,000 uncontended increments per second, well within a single row's atomic-add capacity. No writer ever waits on another, and like latency stays flat even as volume climbs an order of magnitude.

Local aggregation compounds the win. Each application node batches the likes it receives in memory for a short window — 200ms — and flushes the sum to its assigned shard as one atomic add. A node that received 300 likes in that window issues a single +300 against its shard rather than 300 separate updates, cutting store write traffic by two orders of magnitude. The tradeoff is explicit: up to 200ms of increments live only in memory, so a node crash could lose them. For a like count that is an acceptable, bounded loss; for a credit balance it would not be, and that counter would flush every increment durably instead.

Now a read. The post's page wants to show the like total. Rather than sum 50 shards on every one of millions of page loads, a background rollup job sums the shards every few seconds and writes the total to a single cached value; every page read serves that cached number at constant cost, at most a few seconds stale. Meanwhile a parallel question — how many unique users liked the post — is answered not by the sum shards at all but by a HyperLogLog sketch that each write also updates, giving a distinct count with ~2% error in a couple of kilobytes regardless of how many millions liked.

Then the correctness cases. A flaky mobile client resends a like it already sent; because the like carries a unique event id and the system records applied ids, the duplicate is recognized and dropped, so the count does not inflate. Separately, a nightly reconciliation job recomputes the authoritative total from the source-of-truth like events and compares it to the sum of shards; a small drift from a lost in-memory batch is detected and corrected by writing the delta to a shard. The counter thus stays fast under the firehose, cheap to read, and — where it matters — provably accurate, with each of those properties tuned per counter rather than assumed globally.