Design a Real-Time Leaderboard
Ranking 50M players with a SQL query means counting millions of rows for every rank lookup. The design keeps the live board in a Redis sorted set and treats a durable log of score events as the record it can be rebuilt from.
1 · Requirements
The Prompt
Rank 50M players of a mobile game by score in real time. Show the top 10 and each player's own rank, and reset the board every month.
Functional
- Record a player's points when a match ends
- Show the top 10 players for the month
- Show a player's own rank and the players around them
- Start a fresh board each month
Non-Functional
- A new score shows on the board within a second
- Rank lookups return in under 50 ms
- No score is lost or counted twice
2 · Estimates
Drill This Phase First
Show the Answer and Estimates
Answer
About 1,200 a second
10M times 10 is 100M games a day. 100M divided by 86,400 seconds is about 1,160 a second, and about 6,000 at a 5x evening peak.
- Players
- 50M monthly players, about 10M of them active on a given day
- Updates
- 10 games each a day: 100M score updates, about 1,200 a second and 6,000 at a 5x evening peak
- Reads
- Each player checks the board about 10 times a day: another 1,200 reads a second, mostly the cached top 10
- Memory
- 50M entries at about 100 bytes each in a sorted set: roughly 5 GB, which fits on one Redis node
- Throughput
- One Redis node handles about 100,000 sorted set operations a second, so 6,000 writes leave wide headroom
- History
- 100M score events at 50 bytes: about 5 GB a day and 150 GB a month in the durable store
3 · API
POST /scores { player_id, game_id, points, idempotency_key } → { total, rank } (from the game service only)
GET /leaderboard/:month/top?n=10 → { entries[{ rank, player_id, name, score }] }
GET /leaderboard/:month/players/:id → { rank, score, neighbors[] } (4 above and 4 below)
GET /leaderboard/:month/friends/:id → { entries[] } (the player's friends, ranked)Data Model
leaderboard (Redis sorted set, key = "lb:{yyyy-mm}"):
member : player_id
score : points, with an inverted timestamp for ties
score_events (Cassandra, partition = player_id, cluster = ts):
event_id (PK) : uuid idempotency key
player_id : bigint
game_id : bigint
points : int
ts : timestamp
players (SQL):
player_id (PK) : bigint
display_name : string
country : string4 · High-Level Design
5 · Deep Dives
Why a Sorted Set
A SQL query that counts the players with a higher score scans millions of rows for each rank lookup. A Redis sorted set pairs a skip list with a hash map, so ZINCRBY updates a score and ZREVRANK finds a rank in O(log n), about 26 steps for 50M players. The top 10 is ZREVRANGE 0 9.
Durability and Rebuilds
The score store is the record, and Redis is a fast view of it. The writer runs one Lua script that sets the event ID only if absent and then applies ZINCRBY, so a redelivered event never counts twice. If the Redis node is lost, replay the month's events from the score store into a fresh set.
Ties, Resets, and Scale
Pack the points and an inverted timestamp into one score, so the player who reached a total first ranks higher. A new key each month makes the reset free and keeps old boards readable. Past a few hundred million players, shard by score range and add up the sizes of the higher shards to get a global rank.
6 · Wrap Up
The Short Version
A Redis sorted set gives O(log n) score updates and rank lookups. Keep score events in a durable store so the board can be rebuilt, cache the top 10, and shard by score range once one set outgrows a node.