AlgoMaster Logo

Design Distributed Counter

30 min readUpdated June 23, 2026
Listen to this chapter
Unlock Audio

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:

1. Clarifying 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:

After gathering the details, we can summarize the key system requirements.

Functional Requirements
  1. Increment: Increase a counter's value by a specified amount (default: 1).
  2. Decrement: Decrease a counter's value by a specified amount (default: 1).
  3. Read: Retrieve the current value of a counter.
  4. Multi-Counter Support: Each entity can have multiple counter types (likes, views, shares).
Non-Functional Requirements
  1. High Availability: The system must remain operational even during partial failures. Target 99.99% availability.
  2. High Throughput: Handle millions of updates per second across all counters.
  3. Hot Key Resilience: Individual counters must handle thousands of concurrent updates (viral content scenario).
  4. Low Latency: Writes acknowledged in < 50ms, reads served in < 10ms.
  5. Durability: No updates should be lost, even during failures.
  6. Eventual Consistency: Reads may return slightly stale values, but will eventually converge to the correct count.

2. Back-of-the-Envelope Estimation

To understand the scale of our system, let's make some reasonable assumptions.

Write Volume

  • Counter updates: 1 million per second (peak)
  • Average updates: 300,000 per second (steady state)
  • Peak write load (3x factor): 3 million per second (viral event)

Read Volume

  • Assume a 10:1 read/write ratio (counters displayed on every page view)
  • Average read QPS: 3 million per second
  • Peak read QPS: 10 million per second

Counter Count

  • Total entities (posts, videos, users): 10 billion
  • Counter types per entity: 5 (likes, views, shares, comments, saves)
  • Total counters: 50 billion

Storage

Each counter stores:

  • Entity ID (~16 bytes)
  • Counter type (~8 bytes)
  • Counter value (~8 bytes)
  • Metadata (~32 bytes)

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.

Hot Counter Challenge

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.

3. Core APIs

The distributed counter needs a simple but reliable API for counter operations.

1. Increment Counter

Endpoint: POST /counters/{entity_id}/{counter_type}/increment

Increments the specified counter by a given amount.

Request Parameters
  • entity_id (required): The unique identifier for the entity (post ID, video ID, user ID).
  • counter_type (required): The type of counter (likes, views, shares).
  • amount (optional): The increment amount. Default is 1.
  • idempotency_key (optional): Unique key to prevent duplicate increments from retries.
Sample Response
  • success: Boolean indicating the operation was accepted.
  • counter_id: The full counter identifier.
  • acknowledged: Whether the increment was durably recorded.

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.

Error Cases
  • 400 Bad Request: Invalid entity_id or counter_type.
  • 429 Too Many Requests: Client is being rate limited.
  • 503 Service Unavailable: System is overloaded.

2. Decrement Counter

Endpoint: POST /counters/{entity_id}/{counter_type}/decrement

Decrements the specified counter. Identical structure to increment.

Additional Behavior

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.

3. Read Counter

Endpoint: GET /counters/{entity_id}/{counter_type}

Retrieves the current value of a counter.

Request Parameters
  • entity_id (required): The entity identifier.
  • counter_type (required): The counter type.
  • consistency (optional): "eventual" (default, fast) or "strong" (slower, accurate).
Sample Response
  • value: The current counter value.
  • last_updated: When the counter was last modified.
  • is_approximate: Whether the value might be slightly stale.

4. Batch Read Counters

Endpoint: POST /counters/batch

Retrieves multiple counters in a single request (for rendering feeds with many posts).

Request Body
Sample Response

4. High-Level Design

At a high level, our distributed counter system must satisfy three core requirements:

  1. High-Throughput Writes: Accept millions of counter updates per second.
  2. Low-Latency Reads: Serve counter values quickly for page rendering.
  3. Hot Key Handling: Prevent any single counter from becoming a bottleneck.

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.

4.1 Requirement 1: Basic Counter Operations

Let's start with the simplest possible design and identify its limitations.

Naive Approach: Single Database Counter

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.

Components Needed

Counter Service

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.

Write Buffer (Redis)

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.

Persistent Storage (Database)

The durable source of truth for counter values. It stores counter values permanently, survives system restarts and failures, and supports efficient batch updates.

Flow: Incrementing a Counter

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.

Background sync (every few seconds)POST /increment (entity_id, counter_type)INCRBY counter_key 1OK{success: true}Batch UPDATE countersOKReset buffered deltasClientCounter ServiceWrite BufferDatabase
8 / 8
algomaster.io
  1. Client sends an increment request to the Counter Service.
  2. The service performs an atomic INCRBY on the Write Buffer (Redis).
  3. The service immediately acknowledges success to the client.
  4. A background process periodically flushes accumulated deltas to the Database.

This approach provides fast writes by decoupling the client-facing operation from durable persistence.

4.2 Requirement 2: Handling Hot Keys

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.

The Hot Key Problem

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.

Solution: Sharded Counters

Instead of one counter per entity, split each counter into multiple shards.

How Sharded Counters Work

Write Path:

  1. Hash the request (using client ID, request ID, or random selection)
  2. Route to one of N shards: shard = hash(request_id) % num_shards
  3. Increment only that shard

Read Path:

  1. Read all shards for the counter
  2. Sum the values
  3. Return the total

Dynamic Shard Count

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:

  1. Start all counters with 1 shard
  2. Monitor write rate per counter
  3. If write rate exceeds threshold, add more shards
  4. Reduce shards when activity decreases (during background compaction)

Additional Component: Shard Manager

The Shard Manager tracks the shard count per counter, decides when to split or merge shards, and maintains shard metadata.

4.3 Requirement 3: Fast Reads

Summing multiple shards on every read is slow. For a counter with 100 shards, we would need 100 Redis reads, adding significant latency.

Solution: Materialized Read Cache

Maintain a pre-computed total alongside the shards.

Components Needed

Read Cache

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.

Dual-Path Architecture

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.

Write Path

  1. Increment goes to a random shard (distributed load)
  2. Write is acknowledged immediately

Read Path

  1. Read goes directly to the Read Cache
  2. Returns pre-aggregated total in O(1)

Background Aggregation

  1. Periodically (every 1-5 seconds), sum all shards
  2. Update the Read Cache with the new total

This introduces a small lag between writes and read visibility, but keeps reads extremely fast.

4.4 Requirement 4: Durability

So far, everything is in Redis. If Redis crashes, we lose counter data. We need durable persistence.

Solution: Write-Ahead Log + Async Persistence

Components Needed

Write-Ahead Log (WAL)

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.

Persistent Database

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.

Flow: Durable Increment

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.

Background persistenceIncrement requestAppend operationLogged (durable)INCRBY (fast)OKSuccessBatch apply operationsAppliedTruncate applied entriesClientCounter ServiceWrite-Ahead LogWrite BufferDatabaseClientCounter ServiceWrite-Ahead LogWrite BufferDatabase
10 / 10
algomaster.io
  1. Write operation is first appended to the WAL (durable, fast append-only write)
  2. Then applied to Redis (fast, in-memory)
  3. Client receives success acknowledgment
  4. Background process applies WAL entries to the Database in batches
  5. Applied WAL entries are truncated

Recovery Process

  1. On startup, load counter values from Database
  2. Replay any WAL entries not yet applied to Database
  3. Resume normal operation

4.5 Putting It All Together

Here is the complete architecture combining all requirements:

Core Components Summary

ComponentPurpose
Load BalancerDistributes requests across Counter Service instances
Counter ServiceStateless service handling increment/decrement/read operations
Write-Ahead LogDurable log for all write operations (Kafka or similar)
Redis ClusterIn-memory storage for sharded counters
Read CachePre-aggregated counter totals for fast reads
Aggregator ServiceBackground service that sums shards and updates read cache
DatabasePersistent storage for counter values

Data Flow Summary

Scroll
OperationPathLatency
WriteClient → Service → WAL → Redis< 50ms
ReadClient → Service → Read Cache< 10ms
AggregationRedis → Aggregator → Read CacheEvery 1-5 sec
PersistenceWAL → DatabaseEvery 10-60 sec

5. Database Design

5.1 Storage Requirements

The distributed counter has three distinct storage needs:

  1. Hot Storage (Redis): Sharded counters for high-throughput writes
  2. Read Cache (Redis/Memcached): Pre-aggregated totals for fast reads
  3. Cold Storage (Database): Durable, persistent counter values

5.2 Redis Schema

Sharded Counter Keys

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.

Shard Metadata

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.

Read Cache Keys

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.

5.3 Database Schema (PostgreSQL)

1. Counters Table

The source of truth for all counter values.

Scroll
FieldTypeDescription
entity_idVARCHAR(64)Entity identifier (PK part 1)
counter_typeVARCHAR(32)Counter type: likes, views, shares (PK part 2)
valueBIGINTCurrent counter value
versionBIGINTOptimistic locking version
updated_atTIMESTAMPLast update timestamp
created_atTIMESTAMPCreation timestamp

Primary Key: (entity_id, counter_type)

Index: idx_counters_updated on updated_at for finding recently changed counters

2. Counter Events Table (Optional)

For audit trail and analytics, store individual counter events.

Scroll
FieldTypeDescription
idUUIDUnique event identifier (PK)
entity_idVARCHAR(64)Entity identifier
counter_typeVARCHAR(32)Counter type
deltaINTEGERChange amount (+1, -1, etc.)
actor_idVARCHAR(64)Who triggered the change (user ID)
idempotency_keyVARCHAR(64)For deduplication
created_atTIMESTAMPWhen the event occurred

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

6. Design Deep Dive

Now that we have the high-level architecture and database schema in place, let's dive deeper into some critical design choices.

6.1 Counter Architectures

There are several approaches to implementing distributed counters, each with different trade-offs. Understanding these helps you make informed decisions in an interview.

Architecture 1: Single Counter with Locking

The simplest approach: one row per counter with atomic updates.

How It Works

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.

Pros

  • Simple: Easy to implement and understand
  • Strongly consistent: Reads always return accurate values
  • Transactional: Easy to combine with other operations

Cons

  • Lock contention: Concurrent updates serialize on the row lock
  • Limited throughput: Maybe 1,000-5,000 updates/second per counter
  • Database bottleneck: Database becomes single point of failure

Best For

Low-throughput counters (< 100 updates/second), systems requiring strong consistency.

Architecture 2: Sharded Counters

Split each logical counter into multiple physical shards.

How It Works

Write:

Read:

Pros

  • Horizontal scalability: Each shard handles a fraction of the load
  • No lock contention: Different shards can be updated in parallel
  • Adjustable: Add shards for hot counters, reduce for cold ones

Cons

  • Read amplification: Must read all shards to get total
  • Eventual consistency: Aggregated totals may be slightly stale
  • Complexity: Managing shard counts adds operational overhead

Best For

High-throughput counters, viral content scenarios, when eventual consistency is acceptable.

Architecture 3: CRDT Counters

Conflict-free Replicated Data Types (CRDTs) are designed for distributed systems where replicas may diverge and later merge.

How It Works

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:

  • P-Counter for increments
  • N-Counter for decrements
  • Value = sum(P) - sum(N)

Pros

  • Partition tolerant: Works even when nodes cannot communicate
  • No coordination: No locks, no consensus needed
  • Mathematically guaranteed convergence: All replicas eventually agree

Cons

  • Memory overhead: Stores state per node, not just a single value
  • Complexity: More complex to implement correctly
  • No strong consistency: Cannot provide "exactly X" guarantees

Best For

Multi-datacenter deployments, systems prioritizing availability over consistency, offline-capable applications.

Architecture 4: Event Sourcing

Instead of storing counter values, store the events that modify them.

How It Works

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.

Pros

  • Complete audit trail: Know exactly who did what and when
  • Flexible queries: Can compute count at any point in time
  • Enables undo: Can reverse specific actions

Cons

  • Read complexity: Must replay events or maintain materialized views
  • Storage growth: Events accumulate forever (or require snapshotting)
  • Performance: Counting millions of events is slow without optimization

Best For

Systems requiring audit trails, undo functionality, or time-travel queries.

Summary and Recommendation

Scroll
ArchitectureThroughputConsistencyComplexityBest For
Single CounterLowStrongSimpleLow-traffic counters
Sharded CountersHighEventualMediumViral content, social media
CRDT CountersHighEventualHighMulti-datacenter, offline
Event SourcingMediumEventualHighAudit trails, analytics

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.

6.2 Consistency Models

One of the most important decisions in distributed counter design is the consistency guarantee you provide.

Strong Consistency

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.

Implementation

Use a single authoritative database with synchronous replication, route all reads through the primary database, and apply updates using distributed transactions or consensus protocols.

Trade-offs

  • Latency: Reads are slower (database round-trip)
  • Availability: System unavailable if primary is down
  • Throughput: Limited by single-node write capacity

When to Use

  • Financial counters (account balances, inventory stock)
  • Counters used for access control decisions
  • Regulatory requirements demand accuracy

Eventual Consistency

Reads may return stale values, but all replicas will eventually converge to the same value if updates stop.

Implementation

Write to a local replica and propagate asynchronously. Reads are served from any replica, possibly stale, and background processes reconcile differences.

Trade-offs

  • Latency: Very fast reads and writes
  • Availability: System remains available during partitions
  • Accuracy: Values may be temporarily incorrect

When to Use

  • Social media metrics (likes, views, shares)
  • Analytics dashboards
  • Any counter where "approximately correct" is acceptable

Read-Your-Writes Consistency

A middle ground: users always see their own updates, but may see stale data from others.

Implementation

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.

Trade-offs

  • User experience: Users see their actions reflected immediately
  • Complexity: Must track per-user pending writes
  • Partial consistency: Other users' updates may be delayed

When to Use

  • Like buttons (user should see their like immediately)
  • Any counter where user feedback is important

Consistency in Our Design

Our architecture supports configurable consistency:

Scroll
Read TypeHow It WorksLatencyUse Case
EventualRead from Read Cache< 5msPage rendering, feeds
Read-Your-WritesCache + user's pending delta< 10msAfter user action
StrongSum all shards directly< 50msAdmin dashboards, debugging

6.3 Handling Viral Content

When a post goes viral, it might receive 100,000 likes per second. Even with sharding, this is challenging.

The Thundering Herd Problem

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.

Solution 1: Aggressive Sharding

Automatically increase shard count when write rate exceeds thresholds.

Scaling Example

  • 1,000 writes/sec → 1 shard
  • 10,000 writes/sec → 10 shards
  • 100,000 writes/sec → 100 shards

Solution 2: Write Batching at Client

Instead of sending every like individually, batch them at the edge.

Implementation:

Solution 3: Probabilistic Counting

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.

Solution 4: Pre-Provisioned Hot Keys

For predictable viral events (Super Bowl, product launches), pre-provision extra capacity.

6.4 Idempotency and Deduplication

A critical challenge in distributed systems: what happens when a request is retried?

The Problem

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.

Solution 1: Idempotency Keys

Clients include a unique key with each request. The server rejects duplicates.

Solution 2: Actor-Based Deduplication

For user-initiated counters (likes), track which users have already acted.

This naturally prevents duplicate likes from the same user.

Solution 3: Event Log Deduplication

If using event sourcing, deduplicate at the event ingestion layer.

Trade-offs

Scroll
ApproachMemoryComplexityBest For
Idempotency KeysHigh (store all keys)LowAPI-level deduplication
Actor-BasedMedium (store user sets)LowUser actions (likes, votes)
Event LogLow (windowed)HighEvent-sourced systems

7. Follow-ups

7.1 Read Optimization Strategies

Since reads are typically 10x more frequent than writes, optimizing the read path is critical.

Strategy 1: Aggressive Caching with Short TTL

Cache counter values with a short TTL, accepting slight staleness.

Trade-off: Values may be up to 5 seconds stale.

Strategy 2: Background Refresh

Proactively refresh popular counters before cache expires.

This ensures hot counters are always fresh in cache.

Strategy 3: Approximate Counts for Display

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.

Strategy 4: Batch Reads for Feeds

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.

7.2 Failure Handling and Recovery

What happens when parts of the system fail?

Redis Failure

Scenario: Redis cluster becomes unavailable

Impact: Cannot write or read counter values

Mitigation

  1. Fallback to database: Serve reads from persistent storage (slower but available)
  2. Queue writes: Buffer increments in a message queue (Kafka) until Redis recovers
  3. Circuit breaker: Fail fast and return cached/approximate values

Write-Ahead Log Failure

Scenario: Kafka becomes unavailable

Impact: Cannot durably record writes

Mitigation

  1. Synchronous database writes: Fall back to direct database updates (slower)
  2. Local disk buffer: Write to local disk, replay when Kafka recovers
  3. Reject writes: Return 503, let clients retry

Database Failure

Scenario: Primary database is down

Impact: Cannot persist counter values, cannot recover from cold start

Mitigation

  1. Read replicas: Promote a replica to primary
  2. Continue in-memory: Redis has recent values, persist when DB recovers
  3. WAL replay: Replay from Kafka when database is back

Recovery Process

After a failure, the system must reconcile state:

Quiz

Design Distributed Counter Quiz

20 quizzes