Sweaty Imposter

Design a Real-Time Leaderboard

System DesignReal TimeWrite HeavyMid3 min readUpdated

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           : string

4 · High-Level Design

Scores flow from the game service through Kafka into the sorted set, so a new score ranks within a second. Reads are ZREVRANGE for the top 10 and ZREVRANK for one player, each O(log n).

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

Name the next risks: cheaters, so only the game server may post scores; friend boards for players with thousands of friends; and boards by country or region.

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.