System design interview guide
Distributed Counter System Design Interview: Counting Likes and Views Without a Hot Shard
A single viral post can attract millions of likes in a few minutes, and every one of those taps is a write to the same logical number. A naive design routes all of them to one row or one Redis key, which means one shard absorbs the entire storm while the rest of the cluster sits idle. Netflix published that its internal counter service handles roughly 75,000 counter requests per second globally at single-digit millisecond latency, and it gets there by never treating a hot counter as a single contended value.
A distributed counter looks trivial until you notice that popularity is not evenly distributed. Most counters are cold, but a handful go viral and take a firehose of concurrent increments. The core problem is write contention on a single hot key, because consistent hashing maps one key to exactly one node no matter how large the cluster is. The standard answer is to stop storing the count as one value. You either shard the counter into N sub-counters and sum them on read, buffer increments in memory and flush batched deltas, or model the counter as a CRDT so replicas merge without coordination. Reads and writes get very different treatment: writes must be cheap and absorb bursts, while reads tolerate a slightly stale number because nobody notices if a like count is off by a few for a second. For unique counts like distinct viewers you switch to a probabilistic structure such as HyperLogLog. Durability comes from a persistent store behind a fast cache, and time-windowed counts fall out of bucketing events by time. The whole design is an exercise in trading exact, immediate consistency for write throughput and availability.
Asked at: Asked at Meta, Google, Twitter/X, YouTube, Reddit, Uber, and most companies that run a social or content product with visible engagement numbers. It also shows up at Netflix, which published a dedicated counter abstraction, and at any team building analytics or rate-limiting infrastructure.
Why this question is asked
Interviewers like this problem because it hides real depth behind a one-line prompt. Anyone can propose an INCREMENT statement, but the moment you mention a viral post the candidate has to confront hot-key contention, and that pulls in sharding, batching, cache versus durable storage, and the read-versus-write split. It is one of the cleanest ways to test whether someone actually understands eventual consistency rather than reciting the phrase. Strong candidates reach for CRDTs or sharded counters unprompted, explain why exact counts are usually the wrong requirement, and know when to swap in HyperLogLog for unique counting. It also rewards good back-of-the-envelope math and forces an honest discussion of what accuracy the product truly needs.
Requirements
Always clarify these in the first 5 minutes of the interview. Do not start drawing boxes until both lists are agreed.
Functional requirements
- Increment a counter for an entity and interaction type, for example likes on post 123 or views on video 456
- Support decrement for reversible actions such as unliking, without letting the count drift below zero
- Read the current count for one entity with low latency for feed and page rendering
- Batch-read counts for many entities at once, since a feed renders dozens of posts per screen
- Count unique actors where required, for example unique viewers rather than total views
- Expose time-windowed counts such as likes in the last hour or views today
- Guarantee that a single user action is counted at most once even when the client retries
- Let the count converge to a correct value after transient failures rather than losing writes silently
Non-functional requirements
- Absorb write bursts on hot keys without a single shard becoming a bottleneck
- Keep read latency in the low single-digit milliseconds for cached counts
- Stay available for both reads and writes during partial outages, favoring availability over strict consistency
- Tolerate a small, bounded staleness in displayed counts rather than blocking on coordination
- Persist increments durably so a cache crash does not lose engagement data
- Scale horizontally to billions of counters and hundreds of thousands of writes per second
- Provide idempotency so retries and at-least-once delivery do not overcount
- Keep per-counter storage small enough that billions of mostly-cold counters remain cheap
Back-of-envelope scale estimates
Show your math. Pulling numbers from thin air signals you have not thought about the load.
Peak writes on a viral post
~50,000 increments/sec (estimate)
Assume a post reaches 3 million likes in one hour during a spike. 3,000,000 / 3,600 is about 833/sec on average, but engagement is front-loaded, so a peak-to-average factor of 50x gives roughly 50,000/sec on the single hottest key. Derived, illustrative.
Global write throughput
~200,000 increments/sec (estimate)
For a platform with 500 million daily active users each generating about 20 counted interactions per day, that is 10 billion writes/day. 10e9 / 86,400 is about 116,000/sec average, and with a 2x daily peak factor you plan for roughly 230,000/sec. Derived.
Read-to-write ratio
~10:1 to 100:1
Counts are displayed on every feed impression and profile view, while writes happen only when someone acts. Feeds render many counters per scroll, so reads dominate heavily. Estimate; systemdesign.one cites a 10:1 baseline for social counters.
Storage per sharded counter
~0.5 to 2 KB per hot counter (estimate)
A cold counter is one 8-byte integer plus keys and metadata. A hot counter split into 100 shards holds 100 small rows. At roughly 16 bytes of value plus overhead per shard, 100 shards land near 1 to 2 KB. Only the hot minority pay this. Derived.
Unique-viewer counter memory
12 KB per counter with HyperLogLog
Redis HyperLogLog uses a fixed 12 KB per structure regardless of cardinality, versus roughly 400 to 600 MB for a raw set of 10 million member IDs. That is the entire reason approximate counting exists for unique counts. Published figure from Redis docs.
Netflix counter service throughput
~75,000 requests/sec globally
Netflix published this figure for its distributed counter abstraction, served at single-digit millisecond latency across AddCount, GetCount, and related endpoints. Published, not derived.
High-level architecture
A write enters through an API service that identifies the counter by a composite key such as namespace, entity id, and interaction type. Instead of touching one durable row, the write path first hits a fast in-memory tier. For a hot counter the service either picks one of N shard keys at random and increments it, or appends the increment to an in-memory buffer that a background flusher drains as batched deltas. Each increment carries an idempotency token so a retried request does not count twice. The fast tier is a distributed cache such as Redis or EVCache that gives atomic increments and low latency, and behind it sits a durable store, typically a wide-column database like Cassandra, that holds the authoritative value or the log of increment events. A rollup or aggregation process periodically sums the shards or replays the recent event log to produce a materialized count, writes that checkpoint back to the durable store, and refreshes the cache. On the read path the service returns the cached rolled-up value with a quick point read, and for a fresh read it can add the un-aggregated delta on top. Unique counts follow a parallel path that feeds actor ids into a HyperLogLog structure rather than an integer. Time-windowed counts come from bucketing increment events by time bucket so a query can sum the relevant windows. The whole system leans on eventual consistency: the displayed number trails the true number by a bounded margin and converges, which is what lets writes stay cheap and available during bursts and partitions.
In a real interview, sketch this on the whiteboard before diving into any single box.
Core components
Walk through each service. The interviewer wants to hear what each one owns, not just the names.
Counter API service
A stateless service that accepts increment, decrement, and read calls and maps each to a logical counter key. It enforces idempotency by checking the request token, chooses the sharding or batching strategy per counter, and hides all storage detail from callers. Because it is stateless it scales horizontally behind a load balancer.
In-memory fast tier
A distributed cache such as Redis or Netflix EVCache that provides atomic INCR operations and single-digit millisecond access. It absorbs the write firehose and serves the vast majority of reads. It is treated as fast but not authoritative, so a crash costs recent un-flushed deltas unless they were also logged durably.
Sharded counter layer
For hot keys the logical counter is split into N physical sub-counters, for example post:123:likes:0 through post:123:likes:99. Writes spread across shards to avoid a single hotspot, and a read fans out to all shards and sums them. The shard count can grow for keys that trend hot.
Write buffer and batch flusher
A component that coalesces many increments to the same counter into a single delta and flushes it to the durable store on an interval or size threshold. This turns thousands of tiny writes into a few large ones, cutting write amplification dramatically. It trades a small delay in durability for a large gain in throughput.
Durable store and event log
A wide-column database such as Cassandra that holds either the checkpointed count or an append-only log of increment events keyed by time bucket. Bucketing by time and by an extra split key keeps partitions from growing unbounded on a hot counter. This is the source of truth used to rebuild caches and audit counts.
Rollup and aggregation pipeline
A background process that sums shards or replays recent events into a materialized count, writes the checkpoint with a last-rollup timestamp, and updates the cache. It uses in-memory queues partitioned by counter so the same counter always aggregates in one place, and it applies back-pressure so hot counters do not starve cold ones.
Unique-count and window engine
For distinct counting it maintains a HyperLogLog per counter so unique viewers cost a fixed 12 KB instead of storing every id. For time windows it groups events into tumbling or sliding buckets so queries like views-today reduce to summing the relevant buckets.
Data model
Pick the right store per table. Justify each choice with the access pattern, not by reflex.
counter_shardscounter_keyshard_idvalueupdated_atOne row per physical shard of a hot logical counter. Reads sum value across all shard_id for a counter_key. Cold counters use a single shard. shard_id is chosen at random on write to spread load.
counter_eventsnamespacecounter_nametime_bucketevent_bucketevent_iddeltaAppend-only log of individual increments in the eventually-consistent model. time_bucket and event_bucket split partitions so a hot counter never overwhelms one Cassandra partition. event_id plus timestamp forms the idempotency key.
counter_rollupcounter_keylast_rollup_countlast_rollup_tslast_write_tsCheckpoint of the aggregated value and the timestamp up to which events were summed. A read returns last_rollup_count and optionally adds newer events. Uses last-write-wins on last_write_ts to converge safely.
idempotency_tokenstokencounter_keygeneration_timeexpires_atShort-lived record of processed increment tokens so a client retry or an at-least-once redelivery does not double count. Entries expire after a safety window since old duplicates cannot recur.
unique_counterscounter_keyhll_blobestimated_cardinalityupdated_atHolds a HyperLogLog blob per unique counter, for example distinct viewers of a video. Adding a member is constant time and the whole structure is a fixed 12 KB with about 0.81 percent standard error.
Deep dives
These are the conversations the interviewer is steering you toward. Practice each one until you can talk through it without notes.
The hot-key problem and why one row cannot save you
Consistent hashing sends a given key to exactly one node, which is a feature for locality and a disaster for a viral counter. It does not matter that your cluster has 200 nodes, because post:123:likes hashes to one of them and that one node eats every increment. On a relational row the same thing shows up as lock contention: every UPDATE takes a row lock, so concurrent writers serialize and latency climbs while the rest of the database is bored. The fix is to accept that a hot logical counter must be many physical values. You either fan writes across N shard keys so the hashing spreads them, or you stop hitting the store per write at all and buffer in memory. The tell of a weak answer is proposing a bigger machine or a faster database, because vertical scaling does nothing when the whole storm targets a single key.
Sharded counters and the write-cost versus read-cost trade
The sharded counter splits post:123:likes into post:123:likes:0 through post:123:likes:N-1. On increment you pick a shard, usually at random or round-robin, and bump only that one, so writes spread across N keys and therefore across N nodes. The catch is that reads now fan out to all N shards and sum them, turning one read into N reads. So N is a dial: more shards mean more write headroom but more expensive reads. A common refinement is adaptive sharding, where cold counters use one shard and a counter only splits into many when it starts trending hot, so you do not pay the read tax on the millions of counters that never go viral. You also need to size N to the write rate, not to the read rate, since reads are usually served from a single cached rolled-up value rather than by re-summing shards on every request.
Write batching, buffering, and write amplification
Even with sharding, writing to durable storage on every single like is wasteful, because each increment carries fixed per-write overhead in log appends, replication, and compaction. Batching flips this: the service holds increments in an in-memory buffer and flushes a single aggregated delta per counter every interval or once the buffer fills. Ten thousand likes become one write of plus-ten-thousand. This is the same idea as a write-behind cache. The cost is a durability window: if the process crashes before flushing, the un-flushed delta is lost unless you also appended the raw events to a durable log first. Netflix chose in-memory rollup queues over an external durable queue to cut infrastructure cost, and accepts that a crash can drop some in-flight rollup work, then self-heals on the next read by re-aggregating from the durable event store. That is the honest trade: cheaper and simpler, at the price of occasional recomputation.
Eventual versus strong consistency for counts
The single most important requirement clarification in this interview is how correct the count must be and when. For likes and views the honest answer is that approximate and slightly stale is fine, because no user can tell whether a video has 1,000,201 or 1,000,198 views, and the number is always moving anyway. Choosing eventual consistency lets you shard, batch, and stay available during partitions, which is why nearly every production system does it. Strong consistency, where every reader sees every increment immediately, forces coordination such as a consensus round or a distributed lock per increment, and that reintroduces the exact serialization you sharded to avoid. There are narrow cases that genuinely need exactness, like billing meters or inventory decrement or fraud limits, and for those you accept lower throughput, use atomic operations with idempotency, and often keep the count small in blast radius. Knowing which regime the product is in, and saying so out loud, is what separates a senior answer from a junior one.
CRDTs: G-Counter and PN-Counter for conflict-free increments
A conflict-free replicated data type lets multiple replicas take writes independently and merge later with no coordination and a guaranteed converged result. A G-Counter (grow-only) gives each replica its own sub-counter; the value is the sum of all replicas' sub-counters, and merging two states just takes the elementwise maximum, which is why concurrent increments never conflict. It only supports increment, so for likes that can be undone you use a PN-Counter, which is two G-Counters, one for increments and one for decrements, with the value being their difference. This is exactly what you want for multi-region counting: each region increments locally with no cross-region round trip, regions gossip their state, and the merge is deterministic regardless of message order or duplication. The cost is that state grows with the number of replicas and you must bound replica identity, which is why designs like handoff counters exist to keep the identity set from exploding. CRDTs shine when availability and low write latency across regions matter more than a single authoritative writer.
Approximate unique counting with HyperLogLog
Total views is a plain integer, but unique viewers is a set-cardinality problem, and storing every distinct actor id to answer it is enormous. A raw set for 10 million unique visitors costs several hundred megabytes; HyperLogLog answers the same question in a fixed 12 KB with about 0.81 percent standard error. It works by hashing each id and tracking the maximum number of leading zero bits seen, since a longer run of leading zeros is statistically rare and implies a larger set, then averaging across many registers to cut variance. Adds are constant time and the structures merge, so you can maintain per-region HLLs and union them for a global unique count. The limits matter in the interview: it estimates rather than counts exactly, and it cannot remove an element, so it fits unique views and reach but not anything that needs exact membership or deletion. When someone insists on exact unique counts you either keep the set and pay the memory, or push it to a batch job.
Read path, write path, and time-windowed counts
Reads and writes deserve separate designs because their requirements diverge. The write path optimizes for absorbing bursts: cheap, idempotent, buffered or sharded, and durable enough to reconstruct. The read path optimizes for latency and fan-out: a feed asks for dozens of counts at once, so you serve a pre-aggregated value from cache with a single point read, and only fall back to summing shards or replaying events when the cache misses or when a fresh read demands the latest delta on top of the last checkpoint. Time-windowed counts, such as likes in the last hour or views today, come from bucketing increment events by a time key so a windowed query sums the buckets that overlap the range. Tumbling windows give you non-overlapping fixed periods that are cheap to roll up and expire, while sliding windows need either overlapping buckets or per-event timestamps and cost more. Retention policies then delete old buckets so the event store does not grow without bound. This bucketing is also what keeps a hot counter's partition from growing unbounded in the durable store.
Trade-offs to discuss
Every senior interviewer expects you to surface at least 3 of these. Pick the decisions, state the alternatives, and justify your choice.
Sharded counter versus single atomic key
Sharding spreads writes across nodes and removes the hotspot, at the cost of scatter-gather reads and more storage per hot counter. A single atomic key is simpler and reads are trivial, but it serializes all writers on one node and collapses under a viral spike. Shard the hot minority, keep cold counters single.
Eventual consistency versus strong consistency
Eventual consistency gives high write throughput and availability during partitions, which is right for likes and views where small staleness is invisible. Strong consistency gives an always-correct number but needs per-increment coordination that destroys throughput. Reserve strong consistency for billing, inventory, or limits where an off-by-one has real cost.
In-memory batching versus write-per-event durability
Batching in memory turns thousands of writes into one and slashes write amplification, but a crash can lose the un-flushed delta. Writing every event durably first is safe but expensive and reintroduces load. The usual answer is to log events durably and batch the rollup, so you get both throughput and recoverability.
CRDT counters versus a single authoritative writer
CRDTs let every region write locally and merge without coordination, which is ideal for multi-region availability and low latency. The price is state that grows with replica count and the need to bound replica identity. A single authoritative writer is simpler and exact but adds cross-region latency and a failover story.
HyperLogLog versus an exact set for unique counts
HyperLogLog fixes memory at 12 KB per counter with about 0.81 percent error and merges cleanly across regions, but it cannot delete members or give an exact number. An exact set is precise and supports removal but costs hundreds of megabytes at scale. Use HLL for reach and unique views, exact sets only when precision is mandatory.
Redis or EVCache fast tier versus a durable store as source of truth
Serving counts from an in-memory cache gives single-digit millisecond reads and atomic increments, but the cache is not durable and can lose recent state. A durable wide-column store survives crashes but is slower per operation. Pair them: cache for speed, durable log and rollup for truth, and self-heal the cache from the store on miss.
How Distributed Counter actually does it
Netflix published a detailed distributed counter abstraction that offers three modes: a best-effort regional counter backed by EVCache for approximate low-latency needs, an eventually consistent global counter that logs each mutation as an event in its TimeSeries abstraction over Cassandra and aggregates them with a background rollup pipeline, and an experimental accurate counter that adds the un-aggregated delta on top of the last checkpoint. It reports about 75,000 counter requests per second globally at single-digit millisecond latency. Idempotency is enforced by an event id plus generation timestamp so retries do not overcount, partitions are bucketed by time and by an extra split column to avoid wide partitions on hot counters, and rollups run in in-memory queues partitioned by a fast XXHash so the same counter always aggregates in one place. Redis provides atomic increments and a native HyperLogLog type that uses a fixed 12 KB per structure with roughly 0.81 percent standard error, which is the standard building block for approximate unique counts. The broader pattern of sharded counters with periodic aggregation is common across social platforms such as Twitter/X, YouTube, Reddit, and Facebook for likes, views, and reactions.
Lessons to study before this interview
If any of these topics are fuzzy, the interviewer will catch it. Each lesson is 15 to 60 minutes with diagrams, code, and a quiz.
Related system design interview questions
Practice these next. They lean on the same core building blocks as Distributed Counter.