Leaderboard System Design Interview: Real-Time Ranking at Scale
A popular mobile game or fitness app can have tens of millions of players whose scores change constantly during an event. Every one of them wants two things on the same screen: the global top 100, and their own rank among millions of others. Doing both in a few milliseconds, while scores are being written thousands of times per second, is the whole problem. A public estimate that circulates in engineering write-ups is that a Redis sorted set with around 100 million members uses on the order of several gigabytes of memory, so a single node can carry a large board but not an unlimited one.
A leaderboard looks trivial until you attach real numbers to it. The core data structure that makes it tractable is the sorted set, a collection kept in score order that supports O(log N) inserts, O(log N) rank lookups, and O(log N + M) range reads. Redis implements this with a skip list for ordering plus a hash table for O(1) score lookup, which is why ZADD, ZREVRANK, and ZREVRANGE all stay fast even with millions of members. The read path splits into two very different queries: the global top-N, which is cheap because it is one bounded range read, and the 'my rank' query for an arbitrary user, which is the expensive one because it must be answered for every viewer. Writes are heavy and bursty during events, so you buffer and batch score updates and use atomic ZINCRBY to avoid read-modify-write races. As the member count grows past what one node can hold, you shard, and sharding a single global ranking is genuinely hard because Redis Cluster does not split one sorted set across nodes. You either shard by score range, or hash members across shards and merge per-shard results, or fall back to approximate ranks for the deep middle of the board. Time-windowed boards (daily, weekly, all-time) are separate keys with TTLs. The design is a study in picking exact ranks where they matter and approximate ranks where they do not.
Asked at: Asked at gaming companies, social and fitness apps, ad-tech firms, and large product companies such as Meta, Amazon, and Riot-style game backends. It shows up in nearly every mid-level and senior system design loop because it is compact enough to finish in 45 minutes yet deep enough to separate candidates.
Why this question is asked
Interviewers like this problem because it forces you to reason about a data structure and a workload at the same time. A weak answer reaches for a SQL ORDER BY and a LIMIT and stops there. A strong answer recognizes that the hard query is not the top 10, it is the rank of an arbitrary user computed for millions of viewers, and that this is exactly what a sorted set solves in logarithmic time. From there the problem opens into batching write-heavy updates, sharding a structure that does not shard cleanly, deciding when an approximate rank is acceptable, and handling time windows and ties. It tests whether you can separate cheap operations from expensive ones and whether you know when to trade exactness for scale.
Requirements
Always clarify these in the first 5 minutes of the interview. Do not start drawing boxes until both lists are agreed.
Functional requirements
- Update a player's score, either by setting an absolute value or by incrementing an existing score atomically
- Return the top N players by score, in descending order, with their scores
- Return the rank of a specific player, counting from 1 for the highest score
- Return a window of players around a given player, for example the 5 above and 5 below them
- Support multiple leaderboards at once, for example per game, per region, and per event
- Support time-windowed boards: daily, weekly, monthly, and all-time
- Break ties deterministically so two players with equal scores get a stable, well-defined order
- Reset or roll over a board when a window ends without losing the all-time board
- Fetch a specific set of players' ranks and scores in one round trip, for a friends list view
Non-functional requirements
- Read latency for top-N and my-rank should be a few milliseconds at the p99 even under event load
- Handle bursty write load during events, on the order of tens of thousands of score updates per second
- Scale to tens of millions of members per board without a single node becoming a bottleneck
- Stay highly available so a node failure does not take the board offline
- Keep results consistent enough that a player never sees their rank move the wrong direction after a score gain
- Bound memory growth predictably as member count and number of boards grows
- Degrade gracefully: if exact deep ranks are too expensive, return an approximate rank rather than time out
Back-of-envelope scale estimates
Show your math. Pulling numbers from thin air signals you have not thought about the load.
Active players per board
10M to 50M (estimate)
A large game or app during a live event. Not a published figure for any one company, treated as a design target.
Score updates during an event
~30,000 writes/sec (Derived estimate)
10M active players each producing one score change every ~5 minutes during an event is 10,000,000 / 300 = ~33,000 writes/sec sustained, with higher bursts.
Rank read cost per query
~27 comparisons
ZREVRANK is O(log N). For N = 100M members, log2(100,000,000) is about 26.6, so each rank lookup touches on the order of 27 skip-list nodes. Derived from the O(log N) bound stated in the Redis docs.
Memory for a large sorted set
~a few GB for ~100M members (estimate)
Widely cited community estimate of roughly 6GB for 100M members; exact usage depends on member key length and Redis version, so treat as an order-of-magnitude figure, not a guarantee.
Top-N read cost
O(log N + M), M small
ZREVRANGE 0 99 returns the top 100, so M = 100 regardless of board size. The cost is dominated by the fixed log N seek, which is why top-N is cheap and cacheable.
Peak read fan-out
Reads >> writes
Every player viewing the board issues at least a my-rank plus a top-N read. With millions of concurrent viewers this dwarfs the write rate, so the my-rank path is the one to optimize and cache.
High-level architecture
A score event arrives at an API service, usually after the game or app server has validated it so a client cannot post an arbitrary score. The write path does not hit Redis on every single event during a burst; instead updates are buffered and coalesced, either in an in-process batch or through a message queue such as Kafka, so that many small increments for the same player collapse into fewer ZINCRBY or ZADD calls. The score store is a Redis sorted set keyed per board, for example board:global:alltime and board:global:daily:2026-07-10, where the member is the player id and the score is the ranking value. Reads come in two shapes. The top-N read is a single ZREVRANGE 0 N-1 WITHSCORES that is cheap and can be cached at the edge for a second or two because the top of the board changes slowly. The my-rank read is a ZREVRANK for the viewing player plus, optionally, a ZREVRANGE around that rank to show neighbors; this is the query issued by every viewer, so it is the one you protect with caching and, at large scale, with sharding or approximation. A durable copy of scores lives in a primary database or event log so the sorted set can be rebuilt after a restart, since Redis is treated as a fast index rather than the source of truth. When one node can no longer hold the board, a routing layer maps each board or each shard of a board to a Redis node, and the application merges per-shard results for global queries.
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.
Score ingestion and validation service
Receives score events from game or app servers, authenticates them, and rejects impossible values so the board cannot be gamed by a malicious client. It is the only writer to the sorted sets, which keeps update logic in one place.
Write buffer or batch layer
Coalesces bursty updates before they reach Redis, using an in-memory batch or a queue like Kafka. This smooths write-heavy event spikes and lets many increments for one player collapse into a single atomic ZINCRBY, cutting Redis operations sharply.
Redis sorted set store
The ranking index. Each board is a sorted set where the member is the player id and the score is the ranking value. It answers ZADD writes, ZREVRANK rank lookups, and ZREVRANGE top-N and neighbor reads, all in logarithmic time.
Durable score store
A primary database or append-only event log that holds the authoritative score history. Redis is a derived index, so if a node is lost the sorted set is rebuilt from this store rather than trusted as the record of truth.
Shard router and merge layer
Maps boards and board shards to Redis nodes and, for a global board split across shards, gathers each shard's top-K and merges them in the application. It also decides when a query can be answered from one shard versus fanned out to all.
Read cache and edge layer
Caches the slowly changing top-N and, where acceptable, short-lived my-rank results. This absorbs the read fan-out from millions of viewers so the sorted set is not asked to recompute the same top-100 thousands of times per second.
Window and rollover scheduler
Creates daily and weekly keys, sets TTLs on them, and rolls windows over at boundaries. It archives finished windows to the durable store and keeps the all-time board separate so a daily reset never erases lifetime totals.
Data model
Pick the right store per table. Justify each choice with the access pattern, not by reflex.
sorted set: board:{scope}:{window}key = board:global:alltimemember = player_idscore = ranking_valuewindow = alltime | daily:DATE | weekly:ISOWEEKThe core structure. One sorted set per board and window. Redis stores it as a skip list plus a hash table, giving O(log N) writes and rank queries and O(1) score lookups.
player_profile (hash)key = player:{player_id}display_nameavatar_urlcountrylast_updatedDisplay metadata kept out of the sorted set, which holds only id and score. Fetched by id after a rank query so the sorted set stays small and cache-friendly.
score_events (durable log)event_idplayer_idboard_iddelta_or_valueoccurred_atAppend-only source of truth in the primary store or Kafka. Used to rebuild a sorted set after a node loss and to audit disputed scores.
board_configboard_idscopewindow_typetie_break_ruleshard_strategyttl_secondsDescribes each board: whether it is global or regional, its window, how ties break, and how it is sharded. Read by the router and the window scheduler.
shard_mapboard_idshard_idredis_nodekey_patternrange_or_hashMaps a board's shards to Redis nodes. For range sharding it records the score range per shard; for hash sharding it records the hashing rule for player ids.
Deep dives
These are the conversations the interviewer is steering you toward. Practice each one until you can talk through it without notes.
Why a sorted set and how Redis implements it
A sorted set keeps members ordered by a floating-point score and lets you both look a member up by key and walk the set in rank order. Redis implements it with two structures at once: a hash table that maps member to score for O(1) ZSCORE lookups, and a skip list that keeps members in score order for ranked operations. A skip list is a linked list with several express lanes stacked on top, so a search skips over large runs and reaches any position in O(log N) expected time. That is why ZADD is O(log N), ZREVRANK is O(log N), and ZREVRANGE is O(log N + M) where M is the number of elements returned. The dual structure is the whole reason a leaderboard is feasible: without the skip list you would scan the set to find a rank, and without the hash you would scan to find a score. With both, a 100 million member board answers a rank query in roughly 27 comparisons.
The two read paths: top-N versus my-rank
These queries look similar but scale very differently. The top-N query, ZREVRANGE key 0 N-1 WITHSCORES, returns a fixed small slice from a known position, so its cost is O(log N + N) with N tiny and constant. It changes slowly and is the same for every viewer, so you cache it for a second or two and serve it from the edge. The my-rank query, ZREVRANK key player_id, is different because it is unique per viewer. With millions of players each checking their own rank, this is the dominant load. Each call is still O(log N), but the fan-out is enormous. You reduce it by caching a player's rank for a short window, by returning rank plus neighbors in one ZREVRANGE around the player's position, and at extreme scale by accepting an approximate rank for players deep in the standings where exactness does not matter.
Write-heavy score updates and batching
During a live event, scores change constantly and in bursts. Writing to Redis on every raw event wastes operations, especially when a single player scores many times in a few seconds. The fix is to buffer and coalesce. You accumulate deltas per player over a short window and flush them with a single atomic ZINCRBY, which adds to the existing score without a read-modify-write cycle and therefore avoids the race where two updaters both read the old value and one overwrites the other. Routing updates through a queue like Kafka gives you a durable buffer, natural batching, and backpressure so a spike does not overwhelm Redis. The tradeoff is a small ranking delay: a score may take a second or two to appear, which is almost always acceptable for a leaderboard and rarely acceptable for, say, a payment.
Sharding a global board that does not shard cleanly
A sorted set lives on one Redis node, and Redis Cluster does not split a single sorted set across nodes. Once a board outgrows one node you have two honest options. Range sharding puts a score band on each node, for example the top million on shard A and the next band on shard B; global top-N is then a read from the top shard, but rebalancing as scores move across band boundaries is painful. Hash sharding spreads players across shards by hashing the player id, which balances writes evenly, but no shard knows the global order, so a global top-K query must ask every shard for its local top-K and merge the results in the application, which is correct for top-K but does not directly give an exact global rank for an arbitrary mid-board player. Most large designs combine a single authoritative board for the top slice, where exact rank matters, with hash-sharded storage and approximation for the long tail.
Approximate rank for the deep middle
Exact rank matters at the top and near a player's own position, but almost nobody cares whether they are 4,301,918th or 4,302,050th. This is where approximation buys scale. One general approach is bucketing: maintain counts of how many players fall into each score band, so a player's approximate rank is the sum of counts in all higher bands plus their offset within their own band, which is cheap and avoids a global scan. Another general approach uses probabilistic structures such as a count-min sketch or a t-digest to estimate how many scores exceed a value, giving a percentile rather than an exact position. A common pattern is a background job that periodically samples the score distribution per shard and caches it, so a mid-board rank is answered from the cached distribution instead of a live cross-shard count. These are general techniques, not a claim about any specific company's internals, and the price is that the reported rank can be off by a bounded amount.
Time-windowed leaderboards and rollover
Daily, weekly, and all-time boards are simply different keys. A score event is written to every window it belongs to, for example board:global:daily:2026-07-10, board:global:weekly:2026-W28, and board:global:alltime, so the same ZINCRBY runs against each relevant key. Windowed keys carry a TTL a little longer than the window so they expire on their own after the window closes and results are archived, which keeps memory bounded without a manual cleanup job. Rollover is handled by writing to the new window's key as soon as the boundary passes; because keys are date-stamped there is no rename race. The all-time board never expires and is kept separate so a daily reset cannot touch lifetime totals. Fan-in writes cost more, so if a board has many windows you batch the multi-key update in a pipeline or a Lua script to keep it a single round trip.
Tie-breaking and stable ordering
Two players with the same score need a deterministic order, otherwise their positions flip on every query and the UI flickers. Redis breaks ties by comparing member keys lexicographically, which is stable but arbitrary and usually not what a product wants. The common rule is that whoever reached the score first ranks higher. You encode that into the score itself with a composite value: keep the real score in the high-order part and pack an inverted timestamp into the low-order part, so an earlier achiever sorts ahead when scores are equal. Because a Redis score is a 64-bit double with 52 bits of integer precision, you have to budget the bits carefully, reserving enough for the score range and enough for a coarse timestamp. When the combined precision does not fit in a double, the alternative is to keep the timestamp in a side hash and resolve ties in the application after reading the tied slice.
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.
Redis sorted set versus SQL ORDER BY with LIMIT
A relational query can produce a top-N with an index, but computing an arbitrary user's exact rank means counting rows above them, which is expensive for every viewer. The sorted set answers both top-N and rank in O(log N) and absorbs write-heavy updates far better, at the cost of holding the board in memory and treating a database as the durable backup.
Exact rank versus approximate rank
Exact rank is simple and correct but forces expensive cross-shard counting for mid-board players at large scale. Approximation with bucketing or sketches makes the deep middle cheap and keeps the top exact, trading a bounded error nobody notices for a large drop in cost.
Range sharding versus hash sharding
Range sharding keeps global order aligned with shards so top-N is one read, but rebalancing as scores drift across band boundaries is hard. Hash sharding balances write load evenly and is easy to grow, but every global query must fan out and merge, and exact global rank for an arbitrary player is not directly available.
Batched writes versus write-through per event
Batching with ZINCRBY through a queue smooths bursts and cuts Redis operations, at the price of a one or two second lag before a score is reflected. Write-through updates the board instantly but risks overwhelming Redis during an event spike and multiplies operations for players who score rapidly.
Redis as source of truth versus Redis as derived index
Trusting Redis alone is simplest and fastest, but a node loss then loses scores. Treating Redis as a rebuildable index backed by a durable event log adds write cost and complexity but lets you recover the board and audit disputed scores, which is the safer default for anything with stakes.
Caching my-rank versus always live
Caching a player's rank for a few seconds slashes the dominant read load, but a player who just gained points may briefly see a stale position. Always-live rank is perfectly fresh but pays a full ZREVRANK per viewer, which is the load you were trying to avoid.
How Leaderboard actually does it
Redis publishes sorted sets as its recommended structure for leaderboards and documents the exact complexity of the operations you rely on: ZADD and ZREVRANK are O(log N), ZRANGE and ZREVRANGE are O(log N + M), and ZSCORE is O(1), all backed by a skip list plus hash table. Redis's own tutorials show the concrete command set, ZADD or ZINCRBY to write, ZREVRANGE for top-N, and ZREVRANK for a player's position, and AWS documents the same pattern on ElastiCache for gaming leaderboards. The harder parts, sharding a single global ranking and approximating deep ranks, are less standardized, so the sharding-by-hash-and-merge and bucketed-approximation approaches described here are drawn from general system design practice and community engineering write-ups rather than a single company's published internals. Memory figures such as the often-cited few gigabytes for around 100 million members are community estimates and depend on member key length and Redis version.
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 Leaderboard.