We use cookies for site analytics. Accept to help us understand how the site is used. See our Privacy Policy for details.
Redis sorted sets, sharding strategies, real-time vs eventual updates, top-K queries, hot keys, deep pagination - the deceptively-simple problem that breaks at 100M users.
Design a leaderboard for a game (or any ranking system) with 100M users. Users earn points; the leaderboard shows the top-100 globally and each user's own rank + neighbors. Updates must be near-real-time during peak events. The system must handle a release-day spike of 1M users hitting the leaderboard simultaneously without melting the cache.
This is the deceptively-simple problem. A naive Redis-sorted-set implementation works for 1M users. At 100M, it breaks: hot keys, slow ZRANGE queries on deep pages, sharding the sorted set across nodes, real-time global updates - each is a senior-level conversation.
Strong candidates start with ZADD/ZRANGE, then walk through sharding strategies, the hot-key problem, and the trade-off between real-time and eventual consistency. Excellent candidates discuss the "user X's rank" query (which is harder than top-K) and explain why deep pagination requires specialized data structures.
Asking these before diving into a solution is the difference between a "hire" and a "no signal" rating. Pick the questions whose answers would change your design.
Storage
Throughput
Memory pressure
Network
Cache hit rate
The system layers a real-time write path (score submit → score validator → sharded sorted-set store) on top of a fanout cache (top-K aggregator → edge-cached top results). The defining decision is sharding the sorted set: by score range vs by user ID vs unsharded with a single hot leader.
The defining performance challenge: top-K queries are O(log N + K) on a single sorted set, but ranking arbitrary users requires the rank function which costs O(log N) per call. Sharding breaks both: the single sorted set's O(log N) becomes "merge K sorted shards" which is O(K log N) and a coordination headache.
The defining operational property: a leaderboard read should never go to disk on the hot path. The architecture is "everything in memory, sharded by access pattern, fronted by aggressive cache". When that breaks (memory pressure, hot key), the cascade is fast.
Stateless. Validates the score submission (anti-cheat, rate limit), publishes to the score event stream, returns ack. Owns the user's read-your-writes contract via local cache.
Durable log of every score submission. Partitioned by user_id for ordering. Source of truth for leaderboard reconstruction; powers the eventual-consistency replication path.
Consumes the score event stream. Performs ZADD on the appropriate Redis shard (sharded by user_id). Updates per-user score in the durable score store. Handles tie-breaking.
Each shard owns a subset of the leaderboard. Variants: by-user-id sharding (each shard has its own sorted set, top-K requires merge), by-score-range sharding (one shard per range, top-K is reading top shard).
Computes the global top-K from sharded sorted sets. Reads top-K from each shard, merges, takes overall top-K. Caches result with short TTL.
Caches the top-K response with TTL (5-10s). 95%+ hit rate for the most-popular query. Single biggest scaling lever.
Returns any user's current rank. Hits the appropriate Redis shard with ZRANK. For sharded layouts, computes the user's global rank from per-shard rank + shard offset.
Durable record of each user's current score. Source of truth for rebuilds. Updated in lock-step with Redis. On Redis restart, the leaderboard is reconstructed from this store.
Snapshots the full sorted set to S3 for disaster recovery. Required because Redis BGSAVE on a 100GB sorted set is operationally painful; managed at application layer instead.
Routes update events to multiple sorted sets (global, regional, friends, daily, weekly). Each leaderboard has its own sharding strategy.
The subsystems where the interview is actually decided. Skim if you're running short; own these if you want a strong signal.
Redis sorted sets are the single most important data structure for leaderboards. Understanding their guarantees and limits is the foundation.
The data structure
A Redis sorted set is a (member, score) collection where members are unique and scores are doubles. Internally, Redis maintains both a skip list (ordered by score) and a hash table (member → score lookup).
Core operations and complexity
Why this is great for leaderboards
Tie-breaking
ZADD ties on score break by member name (lexicographic). This means user_id ordering matters - "user_a" and "user_b" with the same score get different ranks deterministically.
To break ties by submission time, encode time in the score: score = points × 1e10 + (1e10 - timestamp_seconds). Higher timestamp = lower added value = lower rank for ties.
The 64-bit limit
Redis sorted set scores are 64-bit doubles. ~15 significant digits. Plenty for game scores. Composing a tie-break into the score (above) eats some of the precision but stays safe.
Memory layout
At 100M members, a single sorted set is ~8 GB. Fits on one Redis node, just barely. Above this, sharding is required.
Persistence
For leaderboards: AOF + RDB hybrid. Snapshot every hour; AOF for the diff. Recovery: load latest RDB, replay AOF.
The hot key problem at this layer
A single ZADD per second per shard is fine. 50K ZADDs to one key per second is fine. The hot key problem appears when reads pile on a single shard - addressed in the next deep dive.
ZINCRBY: the atomicity guarantee
The killer feature: increment a score atomically without race conditions. Two concurrent ZINCRBY calls both succeed; the result is deterministic. No need for client-side locks or CAS loops.
Use ZINCRBY for cumulative scores. Use ZADD for absolute scores (rare in games; common in voting / rating systems).
A 100M-entry sorted set on a single Redis node maxes out the box (memory, write QPS, blast radius). Sharding is mandatory at scale; the strategy determines what queries are cheap vs expensive.
Option 1: Shard by user ID (hash partitioning)
Each shard owns a slice of users (hash by user_id mod N). Each shard has its own complete sorted set, but only of "its" users.
Pros:
Cons:
Option 2: Shard by score range
Shard 1: scores 1M+. Shard 2: scores 100K-1M. Shard N: scores 0-100K.
Pros:
Cons:
Option 3: Hybrid (the production answer)
Shard the bulk of users by user_id; promote the top-N (e.g., top-10K) to a single "elite" shard.
The elite shard is a hot key but only for reads (highly cacheable).
Option 4: Approximate ranking (probabilistic structures)
At very large scale, exact ranks aren't worth the operational cost. Use:
Used by some social platforms for "you are top 0.1% of users this week" features. Inexact but cheap.
The "merge top-K from M shards" implementation
Cost: M parallel queries (network-bounded), one sort of M×K elements. For M=10, K=100: 1000 elements sorted in <1ms. Network is the bottleneck.
For top-100, M=10: ~5ms total. For top-10000, this becomes problematic - the network traffic and merge cost grow. For deep ranking, alternative strategies (precomputed snapshots, pagination tokens) win.
Hot keys are the most common cause of leaderboard outages. The Pareto distribution is brutal: 1% of users see 99% of leaderboard reads.
The hot read: top-K
Every user wants to see the global top-100. Without caching, every page load is a Redis query.
Naive: 500K leaderboard views/sec × ZREVRANGE call to one shard = 500K reads/sec on the elite shard. Single Redis instance maxes at ~100K simple reads/sec; ZREVRANGE 0 99 is somewhere between simple and complex - call it 50K/sec. Melts.
Mitigation 1: edge cache
Cache the top-100 JSON response at the CDN with a 5-10s TTL.
Result: 95%+ cache hit rate. Origin Redis sees 25K reads/sec instead of 500K. The single biggest lever; do this first.
Trade: leaderboard updates appear with 5-10s lag for the public top. Users editing their own score still see read-your-writes (different code path).
Mitigation 2: request coalescing
Within the cache layer, if N requests arrive for the same key while one is in flight to origin, send only one to origin and fan out the response. 1000 concurrent misses become 1 origin call + 999 followers.
Available in Varnish, NGINX, CloudFront (request collapsing).
Mitigation 3: replicated read shards
The elite shard has N read replicas. Reads round-robin or hash-route. Writes still go to the leader.
Standard for write-then-read patterns where reads dwarf writes by 100x.
The hot write: a viral user
A streamer hits a major milestone; 1M fans submit "vote up" reactions in 10 seconds. The user's ZINCRBY queue piles up at one shard.
Mitigations:
The slow query: deep range
"Show me ranks 50,000 - 50,100" - ZREVRANGE leaderboard 50000 50100.
Internally, Redis traverses the skip list. O(log N + 100), but with constant factors that get bad: 50,000 hops through pointers ≈ 100us-1ms. Fine for one query; bad if every user wants their personal rank slice.
Mitigations:
The cascade
The dangerous failure mode: a hot key saturates a shard → that shard's other operations slow → upstream timeouts → retries → more load on the hot shard. Death spiral.
Mitigations: aggressive client-side timeouts, circuit breakers around per-shard calls, shed load (return 503 for non-critical reads when latency rises).
Hot-key detection
When a hot key is detected, the platform team's playbook: cache more, replicate the shard, coalesce writes. Each is a runbook step, not a research project.
"How fresh is the leaderboard?" is a product question with sharp infrastructure consequences. Most products underspecify this and pay for it.
Real-time tier (sub-second)
Score submit → leaderboard reflects within 1s.
Cost: every score update is a synchronous write to Redis (or to all replicas). High write QPS demands more shards. Cache TTLs must be short (5-10s) to avoid serving stale data, which means cache hit rate drops, origin load rises.
When needed: live competitive games where the leaderboard is part of the user experience during play.
Near-real-time tier (5-30s)
Score submit → leaderboard reflects within 30s.
Cost: score updates buffer at the ingest layer; flushed to Redis in batches. Cache TTLs can be 30-60s.
When needed: most consumer apps. "I just won, where am I?" feels instant if it appears within a few seconds.
Eventual tier (1-5min)
Score submit → leaderboard reflects within minutes.
Cost: batch processing. Score events accumulated to S3, processed by a Spark / Flink job every minute, leaderboard reconstructed.
When needed: weekly / all-time leaderboards where second-level freshness adds nothing.
The hybrid in practice
Most products run multiple tiers simultaneously:
Each tier uses different infra, with declining cost as freshness relaxes.
Read-your-writes
The single user-facing requirement that must be real-time even if everything else is eventual: the user's own score after they submit. Implementation:
This decouples the user's perceived experience from the global leaderboard's update cadence.
Eventual consistency for the public board
Most users are not watching the leaderboard tick by second. A 30-second lag for "what's the top-100" is invisible to 99% of users. Optimizing for real-time on the public path is wasted money.
The interview signal: candidates who default to "real-time everything" pay 10x more for indistinguishable UX. Senior candidates ask "what does the user actually need?" and tier the freshness accordingly.
Snapshot publishing
For eventual tiers, the cleanest implementation: publish leaderboard snapshots periodically.
Read path is dead simple: GET /leaderboard.json from CDN. Origin load is one query per N minutes regardless of read traffic.
Top-K is easy. Deep pagination is hard. The same data structure that's elegant for the first page degrades for the millionth.
Why deep pagination is hard
ZREVRANGE leaderboard 9000000 9000100 has the same theoretical complexity as ZREVRANGE leaderboard 0 100, but the constant factors are different:
At read scale, this is unacceptable. Each request also does no useful caching (everyone's deep page is different).
Cursor-based pagination
Don't use offsets. Use scores as cursors.
Pros: each query is O(log N + K), independent of page depth. No millions-of-hops problem.
Cons: random-access pagination is impossible (can't jump to "page 5000"). Suitable for sequential browsing only.
For "show me my rank's neighborhood" use case, this is the right tool.
Materialized rank windows
For the deep-ranks-as-public-feature case (e.g., "I'm rank 234,567 - show me 100 above and below"):
Option A: cache pre-computed windows of 100 by rank range. 100M users / 100 per window = 1M windows. Cached with long TTL (5-15min).
Option B: compute on-demand with caching. First query for window N is slow; subsequent are fast.
Option C: limit visible depth ("you can see ranks 1-10K; below that, you only see your own neighborhood").
Most production leaderboards combine A and C: pre-cache the top windows (likely-seen); enforce a depth cap for the rest.
The "user X's exact rank" query
Given a user, return their global rank.
The sharded version is harder: how do you know how many users in other shards have higher scores than this user, without scanning them?
Approach:
Cost: O(M) shard queries per rank lookup, but each is fast (histogram lookup, not full scan).
Approximate ranks (with confidence intervals) are a valid product answer for very deep pages. "You are in approximately the top 0.5%" is more useful than an exact slow rank.
The pagination UX choice
The right answer is often product-side: show top-100 + "your rank" + "your neighborhood". Don't expose deep pagination at all. Vast majority of users don't care about ranks 5000+.
Twitter, Stack Overflow, Reddit all do versions of this: top items by some sort, "your activity" view, no global pagination through millions.
One leaderboard is straightforward. Most products want many: global, regional, friends, daily, weekly, all-time. Each adds operational and consistency complexity.
Multi-dimensional leaderboards
Each is its own sorted set (or set of shards). The score-update path fans out to all relevant leaderboards.
Implementation
Score event consumer reads a "leaderboard membership" mapping for each user (which leaderboards they're in) and writes to each.
For a user in 1 global + 1 regional + 5 friends + 3 categories = 10 leaderboards. 10x write amplification per user score event.
Mitigations:
Time-windowed leaderboards (daily, weekly, all-time)
The classic mobile game pattern: 3 boards per game mode, each with its own reset cadence.
Implementation per board:
leaderboard:daily:2026-05-09. New key per day. Old keys expire (TTL of 7 days for "yesterday's leaderboard").Score updates write to all three keys. 3x write amplification, manageable.
The "reset" is just a new key starting fresh. Old key retained briefly (for "yesterday's winners" UI), then dropped.
Consistent reset timing
A daily leaderboard resetting at midnight UTC means the boundary moment has all of Asia awake and competing with all of Europe. Hot-key risk at the boundary.
Mitigations:
The "snapshot the winners" pattern
On reset, snapshot the top-N as the "weekly winners" / "daily winners" record. This snapshot is durable and queryable for "who won last week".
Implementation:
Friends leaderboards
A user has 50 friends. Leaderboard is the union of their scores. Two implementations:
A. Materialized: maintain a sorted set per user containing their friends' scores. Update on friend's score change → write to the friend's friends' boards. N-fan-out per score event where N = avg friends count. Expensive at scale.
B. Computed at read: on read, query friend list, batch ZSCORE for each friend, sort client-side. O(F) where F = friend count. Fast for small F (~100), tolerable for medium (~1000).
Most products go with B for friend lists < 500. For dense social graphs, A wins despite the fanout cost.
The expired-key cleanup
Time-windowed leaderboards accumulate keys: 365 daily boards/year × N modes × N regions. Without cleanup, key count grows linearly forever.
Redis TTL on each key handles automatic cleanup. Ensure TTLs are set on every key creation; missing TTL = leaked memory.
Sharding strategy
By user_id wins for write distribution and simple add-shard ops. By score range wins for cheap top-K. The hybrid (bulk + elite shard) wins in production - top-K reads from elite, writes shard by user.
Real-time vs eventual updates
Real-time everywhere is 10x more expensive than tiered freshness. Most users don't notice 30s lag on the global board. Reserve real-time for the user's own score (read-your-writes) and the top-100; relax everywhere else.
Cache TTL on top-K
Long TTL (60s+) maximizes hit rate but lags. Short TTL (5s) feels live but increases origin load. 5-10s is the standard sweet spot - feels live to users; absorbs >95% of read traffic.
ZREVRANGE deep pagination vs cursor-based
ZREVRANGE with offsets has expensive deep pages. Cursor-based pagination scales but blocks random access. Most products eliminate the need by limiting visible depth + showing personal neighborhood.
Materialized rank vs computed at read
Materialized (per-user friend boards) wastes write amplification but reads are O(K). Computed at read scales writes but reads cost O(F). Materialized for hot read patterns; computed for cold.
Approximate vs exact rank
Exact rank requires per-shard coordination at scale; approximate (probabilistic structures, percentile bands) is order-of-magnitude cheaper. For "top 0.1%" displays, approximate is the right tool. For competitive ladders, exact is mandatory.
Single-leader Redis vs Redis Cluster
Single-leader is simpler but caps at a single box. Cluster shards keys but adds complexity (cross-key transactions break, multi-key commands restricted). For 100M+ users, cluster is mandatory; below 10M, single-leader + read replicas is enough.
Per-event update vs batched
Per-event keeps freshness high, write QPS high. Batching (every 100ms, write the accumulated delta) cuts QPS by 10-100x at minor freshness cost. Hot users especially benefit from coalescing.
AOF + RDB vs RDB only
AOF for durability (recover with sub-second loss); RDB for fast cold-start. Both is the production answer. AOF only is slow on recovery; RDB only loses up to N minutes of data. Tune AOF fsync per second for the right balance.
Source of truth in Redis vs durable store
Redis as source-of-truth is risky (memory-only data). Durable store (DynamoDB) as source-of-truth + Redis as serving cache survives Redis loss with bounded recovery time. The serious answer.
Be ready for at least three of these. The first one is almost always asked.
Reading is the floor. The interview signal is in walking through this live with someone probing follow-ups. Use the AI mock interview to practice talking through requirements, architecture, and trade-offs out loud.
Start an AI mock interview →