A distributed counter is a system that maintains numeric counts across multiple servers while handling high-throughput increments and decrements.
The core idea seems simple: track how many times something happened. But at scale, counting becomes surprisingly complex. When millions of users simultaneously like a viral post or view a trending video, the counter must handle massive concurrent updates without becoming a bottleneck or losing accuracy.
Popular Examples: YouTube view counts, Twitter/X like counts, Reddit upvotes, Instagram follower counts
In this article, we will explore the high-level design of a distributed counter system.
This problem is a common choice in system design interviews because it touches on fundamental distributed systems concepts: consistency vs availability trade-offs, hot key management, and the CAP theorem in practice.
Let's start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.
Here is an example of how a discussion between the candidate and the interviewer might unfold:
Candidate: "What is the expected scale? How many counter updates per second should the system handle?"
Interviewer: "Let's design for a social media platform scale. Assume 1 million counter updates per second during peak, with some counters being extremely hot, like a viral post getting thousands of likes per second."
Candidate: "What types of operations do we need to support? Just increments, or also decrements and reads?"
Interviewer: "We need increment, decrement, and read operations. Think of use cases like likes (increment/decrement) and view counts (increment only)."
Candidate: "What consistency guarantees do we need? Should reads always return the exact count, or is eventual consistency acceptable?"
Interviewer: "Eventual consistency is acceptable for most use cases. Users can tolerate seeing '1.2M likes' even if the actual count is 1,200,042. However, we should not lose any updates."
Candidate: "Should the system support multiple counters? For example, each post has its own like count, view count, and share count?"
Interviewer: "Yes, we need to support billions of independent counters. Each entity (post, video, user profile) can have multiple counter types."
Candidate: "What are the latency requirements for reads and writes?"
Interviewer: "Writes should be acknowledged quickly, under 50ms. Reads should be very fast, under 10ms, since they happen on every page load."
Candidate: "Do we need to support any analytics, like 'likes in the last hour' or historical counts?"
Interviewer: "For this interview, let's focus on current counts only. Time-windowed analytics can be discussed as an extension."
After gathering the details, we can summarize the key system requirements.
To understand the scale of our system, let's make some reasonable assumptions.
Each counter stores:
Total ≈ 64 bytes per counter
Storage estimate: 50B counters × 64 bytes = 3.2 TB
This fits comfortably in a distributed in-memory store, though we will also need persistent storage for durability.
A viral post might receive 10,000 likes per second on a single counter. This is the core challenge: how to handle extreme write concentration on one key.
The distributed counter needs a simple but reliable API for counter operations.
POST /counters/{entity_id}/{counter_type}/incrementIncrements the specified counter by a given amount.
Note: The response does not return the new count to avoid read-after-write latency. Clients should use the read API if they need the current value.
400 Bad Request: Invalid entity_id or counter_type.429 Too Many Requests: Client is being rate limited.503 Service Unavailable: System is overloaded.POST /counters/{entity_id}/{counter_type}/decrementDecrements the specified counter. Identical structure to increment.
Counters may or may not be allowed to go negative, depending on business rules. For like counts, decrement represents an "unlike" and should not go below zero.
GET /counters/{entity_id}/{counter_type}Retrieves the current value of a counter.
POST /counters/batchRetrieves multiple counters in a single request (for rendering feeds with many posts).
At a high level, our distributed counter system must satisfy three core requirements:
The key insight is that writes and reads have very different characteristics. Writes need durability and can tolerate slight delays in visibility. Reads need speed and can tolerate slight staleness. This suggests separating the write and read paths.
Note: Instead of presenting the full architecture at once, we will build it incrementally by addressing one requirement at a time. This approach is easier to follow and mirrors how you would explain the design in an interview.
Let's start with the simplest possible design and identify its limitations.
The most straightforward approach is storing counters in a database and using atomic increment operations.
Why This Fails: All updates to a single counter serialize on one database row, so concurrent updates wait on the row lock. One database also cannot handle millions of QPS, making it a single point of failure.
This approach might work for 100 updates/second, but fails catastrophically for viral content.
A stateless service layer that handles counter operations and routes them appropriately. It receives increment, decrement, and read requests, validates inputs, routes each request to the appropriate storage layer, and handles retries and failures gracefully.
An in-memory buffer that absorbs high-throughput writes before persisting to durable storage. It accepts counter increments with sub-millisecond latency, batches updates for efficient persistence, and provides atomic increment operations.
The durable source of truth for counter values. It stores counter values permanently, survives system restarts and failures, and supports efficient batch updates.
This sequence traces a single increment from client request through the Write Buffer to background database sync, showing where each acknowledgment happens along the path.
INCRBY on the Write Buffer (Redis).This approach provides fast writes by decoupling the client-facing operation from durable persistence.
The basic design still has a problem: a single Redis key for a viral post becomes a bottleneck. Redis is single-threaded per key, so thousands of concurrent increments on one key will serialize.
For example, a celebrity posts a photo and 50,000 users like it within the first second. All 50,000 increments hit the same Redis key. Redis can handle maybe 100,000 ops/sec total, but concentrated on one key, it becomes the bottleneck.
Instead of one counter per entity, split each counter into multiple shards.
Write Path:
shard = hash(request_id) % num_shardsRead Path:
Not all counters need the same number of shards. A new post with 10 likes/day works fine with 1 shard, while a viral post with 10,000 likes/second needs 100 or more shards.
Adaptive Sharding:
The Shard Manager tracks the shard count per counter, decides when to split or merge shards, and maintains shard metadata.
Summing multiple shards on every read is slow. For a counter with 100 shards, we would need 100 Redis reads, adding significant latency.
Maintain a pre-computed total alongside the shards.
A separate cache layer storing pre-aggregated counter totals. It stores the current total for fast O(1) reads, updates asynchronously as writes occur, and serves reads with sub-millisecond latency.
Writes and reads travel through completely separate paths, with a background Aggregator acting as the bridge that keeps the Read Cache in sync with the sharded counters.
This introduces a small lag between writes and read visibility, but keeps reads extremely fast.
So far, everything is in Redis. If Redis crashes, we lose counter data. We need durable persistence.
A durable, append-only log that records every counter operation before it is applied. It durably records every increment and decrement, enables recovery after crashes, and supports replay for consistency.
The long-term storage for counter values. It stores finalized counter values, serves as the source of truth for recovery, and supports efficient batch updates.
This sequence shows the ordering that guarantees durability: the WAL is written before Redis is updated, so the operation survives a Redis crash even if the acknowledgment has already been sent to the client.
Here is the complete architecture combining all requirements:
The distributed counter has three distinct storage needs:
Each physical shard is stored as a separate Redis string key, with the shard index embedded in the key name so the Counter Service can address any shard directly.
Alongside the value shards, a separate Redis hash stores the current shard count and compaction timestamps, giving the Shard Manager the context it needs to decide when to split or merge.
The pre-aggregated total lives under a :total key with a rolling TTL, so reads always see a single integer rather than summing shards on every request.
The source of truth for all counter values.
Primary Key: (entity_id, counter_type)
Index: idx_counters_updated on updated_at for finding recently changed counters
For audit trail and analytics, store individual counter events.
Index: idx_events_entity on (entity_id, counter_type, created_at) for time-range queries
Partitioning: Partition by created_at (daily or weekly) for efficient cleanup of old events
Now that we have the high-level architecture and database schema in place, let's dive deeper into some critical design choices.
There are several approaches to implementing distributed counters, each with different trade-offs. Understanding these helps you make informed decisions in an interview.
The simplest approach: one row per counter with atomic updates.
A single SQL UPDATE with an inline arithmetic expression is all it takes: the database applies the increment atomically by acquiring a row-level lock for the duration of the write.
Low-throughput counters (< 100 updates/second), systems requiring strong consistency.
Split each logical counter into multiple physical shards.
Write:
Read:
High-throughput counters, viral content scenarios, when eventual consistency is acceptable.
Conflict-free Replicated Data Types (CRDTs) are designed for distributed systems where replicas may diverge and later merge.
A G-Counter (Grow-only Counter) uses per-node counters:
Each node only increments its own slot. Merging takes the maximum of each slot:
A PN-Counter (Positive-Negative Counter) supports decrements by maintaining two G-Counters:
Multi-datacenter deployments, systems prioritizing availability over consistency, offline-capable applications.
Instead of storing counter values, store the events that modify them.
Rather than maintaining a running total, the system appends immutable increment and decrement records. The current counter value is derived by replaying the log, or read from a materialized view that is kept in sync with new events.
Systems requiring audit trails, undo functionality, or time-travel queries.
Recommendation: Use Sharded Counters as the default for high-scale systems. The complexity is manageable, and it handles hot keys well. Add event sourcing if you need audit trails for compliance or analytics.
One of the most important decisions in distributed counter design is the consistency guarantee you provide.
Every read returns the most recent write. If user A increments a counter and user B reads it immediately after, B sees the incremented value.
Use a single authoritative database with synchronous replication, route all reads through the primary database, and apply updates using distributed transactions or consensus protocols.
Reads may return stale values, but all replicas will eventually converge to the same value if updates stop.
Write to a local replica and propagate asynchronously. Reads are served from any replica, possibly stale, and background processes reconcile differences.
A middle ground: users always see their own updates, but may see stale data from others.
To achieve read-your-writes, the service fetches the cached total and then adds any pending delta for the requesting user before returning the value, without touching the shared shard layer.
Our architecture supports configurable consistency:
When a post goes viral, it might receive 100,000 likes per second. Even with sharding, this is challenging.
A viral event does not produce a steady load; it produces an exponential spike in the first few seconds, overwhelming whatever capacity was provisioned for normal traffic.
Automatically increase shard count when write rate exceeds thresholds.
Instead of sending every like individually, batch them at the edge.
Implementation:
For extremely high-throughput counters where exact accuracy is not needed, use probabilistic counting.
Approach: Only record a sample of events and multiply:
Trade-off: Less accurate for small counts, but reduces write load by 100x.
YouTube famously uses this for view counts on viral videos.
For predictable viral events (Super Bowl, product launches), pre-provision extra capacity.
A critical challenge in distributed systems: what happens when a request is retried?
Network failures during an in-flight request create ambiguity: the client cannot tell whether the server applied the update before the connection dropped, so retrying risks applying the same increment twice.
Clients include a unique key with each request. The server rejects duplicates.
For user-initiated counters (likes), track which users have already acted.
This naturally prevents duplicate likes from the same user.
If using event sourcing, deduplicate at the event ingestion layer.
Since reads are typically 10x more frequent than writes, optimizing the read path is critical.
Cache counter values with a short TTL, accepting slight staleness.
Trade-off: Values may be up to 5 seconds stale.
Proactively refresh popular counters before cache expires.
This ensures hot counters are always fresh in cache.
For UI display, exact counts often do not matter. Show human-friendly approximations.
When displaying "3.2M likes", being off by a few thousand does not matter.
When rendering a feed with many posts, batch all counter reads into one request.
This fetches 100 counters in 1 Redis round-trip instead of 100.
What happens when parts of the system fail?
Scenario: Redis cluster becomes unavailable
Impact: Cannot write or read counter values
Scenario: Kafka becomes unavailable
Impact: Cannot durably record writes
Scenario: Primary database is down
Impact: Cannot persist counter values, cannot recover from cold start
After a failure, the system must reconcile state:
20 quizzes