Design a Real-Time Gaming Leaderboard
Problem
Design the leaderboard for a popular online game. When a player finishes a match, their score is added to their total for the current season. Players can see the top 100 players, their own exact rank, and the players ranked just above and below them. Rankings must update in near real time. The game has 50 million monthly active players, 25 million of whom play during a season, producing up to 50,000 score updates per second at peak. Leaderboards reset at the start of each monthly season.
- Is the score a running total of points, or the best single match score?
- How are ties broken: by who reached the score first?
- Do we need friend leaderboards and regional leaderboards, or only one global board?
- How fresh must rankings be: within a second, or within a minute?
- Must we keep past seasons' final standings, and for how long?
- How are scores validated against cheating before they reach the leaderboard?
Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.
- Add a player's match score to their season total.
- Return the top 100 players for the current season.
- Return any player's exact rank and score.
- Return the players immediately above and below a given player.
- Reset rankings at the start of each monthly season.
- Support friend leaderboards ranking a player among their friends.
- Archive final standings of past seasons.
Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.
- Up to 50,000 score updates per second at peak.
- Rank changes visible within about one second.
- Top 100 and rank queries under 50 ms at p99, at about 100,000 reads per second.
- Leaderboard memory: 25 million players with an 8 byte score and a 16 byte id plus sorted set overhead is roughly 2.5 GB, which fits on one large in-memory node.
- Scores are durable: losing the in-memory leaderboard must not lose any player's points.
- Ties are broken by whoever reached the score first.
Match results flow from game servers through a validation service into a durable score store and, at the same time, into a Redis sorted set per season that keeps every player ordered by score. Sorted sets give logarithmic updates and rank lookups, so top 100, exact rank, and neighbor queries are all cheap. The durable store is the source of truth, and the sorted set can be rebuilt from it if a node is lost.
Loading diagram...
Score update. When a match ends, the authoritative game server publishes a signed match result with the player ids and points. The score service consumes it, checks that the match id has not been processed before, atomically increments each player's season total in the score store, and then writes the new total (encoded as a composite score) to the season's sorted set with ZADD GT, which only ever raises a player's score.
Top 100. The client calls GET /leaderboard/top. The leaderboard service reads the top 100 cache, which is refreshed every second from ZREVRANGE 0 99, and joins player display names and avatars from the profile store.
My rank. The client calls GET /leaderboard/me. The service runs ZREVRANK for the player to get their position, and ZREVRANGE over a window of 5 positions on either side to return their neighbors.
Season reset. At the season boundary, the scheduler starts writing to a new sorted set key for the new season. The old set is frozen, its final standings are exported to the archive, and the key is deleted after a grace period.
Validates match results and applies score increments to both the durable store and the live leaderboard.
- Kafka for the match result stream, partitioned by match id.
- Stateless consumers in Go or Java.
Scalability
50,000 updates per second spread across many consumer instances. Increments in the score store are commutative, and totals only grow, so ZADD GT makes out-of-order writes from two consumers harmless: an older, smaller total can never overwrite a newer one.
Tradeoffs
Only authoritative game servers can publish results; clients never submit their own scores, which removes the most common cheating vector. Recording processed match ids makes the consumer idempotent, so Kafka redelivery never double counts a match.
In-memory ranked index of every player in the current season.
- Redis sorted sets, one key per season and leaderboard (for example
lb:season:42:global).
Diagram
Loading diagram...
Data Model
- Sorted set member: player id. Score: a composite value
points * 2^22 + (seasonEnd - lastUpdateSeconds). A month is under 2^22 seconds, so more points always means a higher score, the earlier player wins a tie, and the value stays within the 53 bit precision of a Redis score.
Scalability
A single Redis primary handles 50,000 ZADD calls per second and holds 25 million members in a few gigabytes. Read replicas absorb rank queries. If the game grows past one node, the set is partitioned by score range: each shard owns a band of scores, and a player's global rank is the count of players in higher bands (kept in a small summary) plus their rank within their own shard.
Tradeoffs
Sorted sets are chosen over a relational query such as SELECT COUNT(*) WHERE score > ?, which needs an index scan proportional to rank and becomes slow for players far down the list. Sharding by player id hash was rejected because computing a global rank would require querying every shard. For hundreds of millions of players where exact ranks deep in the list are not needed, an approximate rank from a score histogram is a cheaper alternative.
Durable source of truth for each player's season total.
- DynamoDB or Cassandra, keyed by
(season_id, player_id).
Data Model
season_scores(season_id, player_id, points, last_updated_at)processed_matches(match_id, processed_at)with a TTL of a few days.
Scalability
Writes are single-key atomic increments that return the new total, which these stores spread evenly by player id.
Tradeoffs
Redis persistence alone is not trusted for player scores, because a failover can lose the last second of writes. Keeping a durable store means a lost Redis node can be rebuilt by streaming all season scores back into a fresh sorted set, which takes minutes for 25 million rows.
Read API that combines ranks with player profiles.
API
GET /leaderboard/top?season=&limit=100returns ranked players.GET /leaderboard/me?season=returns{ rank, score, above, below }.GET /leaderboard/friends?season=returns the player's friends ranked.
Scalability
The top 100 changes slowly relative to read volume, so it is cached for one second and served to every caller. Friend leaderboards fetch the friend list (typically under a few hundred ids) and look up their scores with one ZMSCORE call, then sort in memory.
Tradeoffs
Friend boards are computed on read rather than maintaining a sorted set per player, which would multiply every score update by the number of followers. With small friend lists, the read-time cost is negligible.
Starts and closes seasons and archives final standings.
- Cron-style scheduler plus a batch export job writing to object storage or a relational archive table.
Tradeoffs
Using a new key per season instead of clearing the existing set makes the reset instantaneous and leaves the previous season available for export, at the cost of briefly holding two seasons in memory.