MediumProblem 34 of 36 in the system design series
Design a Game Leaderboard: Real-Time Ranks for 100M Players

Almost every competitive mobile game has a leaderboard screen. It lists the best players on the planet, shows where you personally stand, and lets you see whether you beat your friends this week. Ordering a list by score is a first-year programming exercise, so the question can look too easy at first glance.
The difficulty is the combination of size, write rate and latency. Picture 100 million ranked players, scores changing thousands of times per second, and a player who expects to see "you are now #48,213" the moment a match ends. Running a sort query on a relational table for that request would take far too long, and recalculating ranks in batches would make the board feel frozen. Interviewers want to see a candidate pick a data structure that maintains order incrementally, then reason about what happens when that structure no longer fits on one machine, how time-bounded boards are handled, and how scores are protected from cheaters.
This is an interview practice design for a Game Leaderboard-like service, built from first principles. It is not a description of how Game Leaderboard is actually built.
Functional Requirements
The system records a score after each completed match, and on the global board only a player's personal best counts, so a bad game never pulls someone down. Anyone can view the global top 100. Each player can also see their exact position, not just a vague bucket, along with a handful of players directly above and below, which gives a sense of who to chase next.
Two extra views make the board social and keep it fresh. A friends leaderboard ranks a player only against people they know, which is often more motivating than a global list of strangers. Daily and weekly boards reset on a schedule, giving newer or casual players a realistic chance of reaching the top for a while.
Record scores
After every match, the player's score is updated.
best score counts
Global top N
Show the top one hundred players in the world.
top 100
My rank
Show my exact rank, and the players just around me.
rank #48,213
Friends & weekly boards
Rank me among friends, and for today or this week.
friendsdaily · weekly
Non-Functional Requirements
Rank changes should be visible within about a second of a match ending, otherwise the board feels disconnected from play. Reads for the top 100 and a player's own rank should complete in under 50 milliseconds, since players open the screen often and expect it to snap into view.
Trust is a requirement in its own right. If the mobile client could submit scores, someone would quickly post an impossible number and hold first place forever, so only authoritative game servers may report results. Durability completes the list. The ranking structure lives in memory for speed, but every score is also persisted, so if the in-memory layer fails we can reconstruct every board without losing anyone's progress.
Real-time updates
A new score should change the rank almost immediately.
< 1 s
Fast reads
Top N and my rank must load quickly.
< 50 ms
Trusted scores
Only game servers report scores, never the client.
anti-cheat
Durable
If the cache dies, we can rebuild every ranking.
no lost scores
Scale Estimation
Assume 100 million players on the global board, with around 20 million playing on a given day and finishing about ten matches each. That yields roughly 200 million score updates per day, near 2.3K per second on average and about 10K at peak. Leaderboard views are lower: assume about 50 million a day, close to 580 per second with peaks around 3K.
Memory is the interesting figure. An entry in an in-memory sorted structure costs roughly 100 bytes including overhead, so 100 million players need about 10 GB. That fits on one server today, which is encouraging, but a breakout game could grow tenfold, and a single node is also a single point of failure. The design should work on one node yet leave a clear path to sharding and replication.
100million
Players on the board
20M play every day
2.3K / sec
Score updates
200M / day · ≈ 10K peak
580/ sec
Leaderboard views
50M / day · ≈ 3K peak
10GB
Memory for ranking
≈ 100 bytes per player
Fits in memory → but plan for growth and for node failures
Core Entities & API
Three entities are enough: Player, Score (one per match) and Leaderboard. A leaderboard ID identifies which ranking is being asked about, such as the global board, the current week or a player's friends, so the same endpoints serve every variant.
POST /scores is internal and callable only by game servers; it carries the player ID, the match ID and the score, and returns 202 because the update is processed asynchronously. Including the match ID lets the updater ignore duplicates if a server retries. GET /leaderboards/{id}/top?n=100 returns ranked entries with player, score and position. GET /leaderboards/{id}/me returns the caller's rank, score and an around_me list of nearby players.
Core entities
PlayerScoreLeaderboard
API
- POST /scores (game server only) { player_id, match_id, score }→ 202 accepted
- GET /leaderboards/{id}/top?n=100→ [{ player, score, rank }]
- GET /leaderboards/{id}/me→ { rank, score, around_me[] }
High-Level Design
Matches run on game servers, and when one finishes the server, not the phone, publishes the result to a Kafka topic. A score updater consumes the topic at its own pace, so a sudden burst of match completions queues up harmlessly instead of overwhelming storage.
For each event, the updater first writes the score to the score database, which is the durable source of truth, and then applies it to Redis sorted sets that hold the live rankings. On the read side, the client goes through the API gateway to the leaderboard API, which answers top-N and rank queries directly from Redis. When a friends board is requested, the leaderboard API asks the friends service for the player's friend list and then looks up those players' scores. Redis provides speed; the database guarantees nothing is lost.
Redis ranks fast · the database is the source of truth
Deep Dive 1 — Redis Sorted Sets
A Redis sorted set stores members ordered by a numeric score and supports exactly the operations a leaderboard needs. Recording a result uses ZADD with the GT flag, which only overwrites the stored value when the new score is higher, so best-score semantics come for free. ZREVRANGE board 0 99 WITHSCORES returns the top 100 from highest down.
A single player's position comes from ZREVRANK, which is exact even across 100 million members. To show the neighbourhood, take that rank and read a short range around it, five positions either side. Ties need a rule, typically that whoever reached the score first ranks higher. Encoding the points in the high part of a number and an inverted timestamp in the low part produces one composite value that sorts correctly.
All of this is fast because a sorted set combines a skip list, which keeps members in order, with a hash map for direct member lookup. Inserts, rank lookups and range reads are therefore O(log N), which stays in the microsecond range even for very large boards.
- 1Add a scoreZADD board GT score player · O(log N)
- 2Top 100ZREVRANGE board 0 99 WITHSCORES
- 3My rankZREVRANK board player · O(log N)
- 4Players around meZREVRANGE from rank − 5 to rank + 5
- 5Break tiesComposite score: points first, then earlier time
- Why fast? A sorted set is a skip list + hash map → every step is O(log N)
Deep Dive 2 — Sharding the Global Board
Keeping one huge sorted set gives exact answers with no extra logic, but it is capped by one machine's memory and CPU and concentrates every request on a single hot node. Sharding by score range makes rank easy to compute, yet the shard holding the top scores receives most of the traffic, and players constantly migrate between shards as their scores rise.
Sharding by a hash of the player ID spreads both memory and load evenly, so it is the chosen option. The top 100 is assembled by asking every shard for its local top 100 and merging the results; because almost everyone requests this list, the merged answer is cached for one second. An exact rank is the sum, across shards, of how many players have a higher score, found with ZCOUNT. With 16 shards queried in parallel this still completes in a few milliseconds.
Precise ranks matter much less deep in the list. A player at position 50 million gains little from the exact number, so the system can show a percentile such as "top 12%" computed from a score histogram, which avoids querying every shard.
One big sorted set
- + Simple, exact ranks
- − One node's memory & CPU
- − Single hot spot
Shard by score range
- + Rank = shards above + local
- − Top shard gets all the hits
- − Players move between shards
Shard by player hashChosen
- + Even load and memory
- + Top N: merge shard tops
- − Rank sums every shard
- Top 100: ask each shard for its top 100, merge, and cache it for 1 second
- My rank = sum of ZCOUNT(my score → max) across all shards
- Far from the top? Show 'top 12%' from a score histogram instead
Deep Dive 3 — Friends & Time Windows
Time-bounded boards are just additional sorted sets whose keys include the period, such as a daily key with the date and a weekly key with the ISO week number. The updater writes each score to the global, daily and weekly sets together. Each period key gets a TTL, and before it expires a job copies the final standings and winners to the database for rewards and history.
Friends boards are computed at read time rather than stored. A typical player has around 200 friends, so the API fetches their IDs, retrieves all their scores in one ZMSCORE call and sorts that small list in memory. Since players reopen this view frequently, each result is cached for a few seconds. If Redis is ever lost, every board can be rebuilt from the score database or by replaying the Kafka topic.
One key per time window
board:daily:2026-10-13, board:weekly:2026-W42 — a new key each period.
Expire old windows
Set a TTL so old boards clean themselves up; save winners first.
Friends board on the fly
Get about 200 friend IDs, fetch scores with ZMSCORE, sort in memory.
Cache the friends board
Keep each player's friends board for a few seconds.
Rebuild from the database
If Redis is lost, replay scores from the DB or Kafka.
Trade-offs
Keeping rankings in Redis makes them instant but costs more per gigabyte than disk, so the database remains authoritative. Exact ranks require consulting every shard, while percentiles are cheaper and good enough for players far from the top. Computing friends boards on demand keeps them simple and always current, at the cost of slightly more work per request.
Routing updates through Kafka smooths bursts, but a rank may trail the latest match by a fraction of a second. Put together: use sorted sets for speed, add shards as the player base grows, and rely on durable storage as the record of truth.
Redis in memory vs database
Instant ranks, but memory costs more, so the DB stays the source of truth.
Exact rank vs approximate rank
Exact needs every shard; a percentile is cheaper and good enough far down.
Friends computed on read
Simple and always fresh, but costs a little more work per request.
Async updates through Kafka
Absorbs bursts, but ranks can lag by a fraction of a second.
Sorted sets for speed. Shards for growth. The database for truth.
Key takeaways
- Redis sorted sets give O(log N) inserts, rank lookups and top-N range reads.
- Use ZADD with GT to keep only each player's best score on the global board.
- Shard by player hash; merge per-shard top lists and sum per-shard counts for rank.
- Daily and weekly boards are separate keys with a TTL; friends boards are built on read.
- Only game servers report scores, and the database can rebuild every board.
Interview questions
What data structure is best for a real-time leaderboard?
A sorted set, such as the one Redis provides, is the usual choice. It keeps members ordered by score using a skip list plus a hash map, so inserting a score, finding a player's rank and reading the top N all run in O(log N) time. That keeps responses fast even with around 100 million players.
How do you get a player's rank among millions of users quickly?
On a single Redis node, ZREVRANK returns the exact position directly. When the board is sharded by player hash, each shard counts how many members have a higher score using ZCOUNT, and the API sums those counts. For players far down the list, a percentile from a score histogram is cheaper and still meaningful.
How do you scale a leaderboard beyond one Redis server?
Shard the board by a hash of the player ID so memory and traffic spread evenly. Build the global top 100 by merging each shard's local top 100 and caching it briefly. Compute exact ranks by summing higher-score counts across shards in parallel. Replicas protect against node failure, and the database can rebuild any shard.
How do you implement daily and weekly leaderboards?
Give every period its own sorted set, keyed by its date or ISO week number, and write every new score to the global, daily and weekly sets. Give each period key an expiry so old boards disappear automatically, and save the final standings to the database before they expire for rewards and history.
How do leaderboards prevent cheating on scores?
Clients never submit scores. Only trusted game servers, which run or validate the match, publish results, and the scores endpoint is internal. Each update carries a match ID so duplicates can be ignored. Durable storage of every score also allows audits and lets the team remove fraudulent entries and rebuild rankings.
Keep practising
- Learn the building blocks in the free system design course.
- Rehearse answers with system design interview questions.
- See which open jobs fit your skills: upload your resume.