The 100 problems, solved / Social and feeds / 025 · Design a real-time game leaderboard

Design a real-time game leaderboard

Rank millions of players globally and show a player's rank and neighbours instantly.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements

Functional

  • Players earn points; the game shows a global top 100, a player's own rank, and the few players ranked just above and below them.
  • Leaderboards per region and per friend list; a new board each day and week, with the previous ones frozen.
  • Scores update in real time: a player who just scored sees their new rank within a second.
  • Out of scope: anti-cheat decisions (treated as a flag that excludes a score), prizes.

Non-functional

  • 50 million daily players (Pokemon Go scale); 10 million concurrent at peak; 500 million score updates a day.
  • Rank reads under 50 ms at p99; top-100 reads served from cache; score writes under 100 ms.
  • Ranks may be approximate for the long tail (rank 3,482,117 may be off by a few) but exact in the top 1,000.
  • No lost scores; a region failure degrades to stale boards, never to no boards.
Back of the envelope

Updates: 5 × 108 / 86,400 ≈ 5,800/s average, 50,000/s at the evening peak. Rank reads: assume 20 per player per day → 109/day ≈ 11,600/s average, 100,000/s at peak.

A sorted set of 50 million (score, player) entries at about 64 bytes each (skip list overhead included) is 3.2 GB: one machine's memory, comfortably. Per-region boards are smaller; friend boards are tiny.

A single-machine sorted set sustains on the order of 100,000 operations a second, so the global board is within one node's capacity at peak but with no headroom, which is why the design shards or partitions by score range.

2. Core entities

  • Score event: player id, points, time, match id (for idempotency), flags.
  • Board: id (scope + period, for example global-2026-11-01), state (live, frozen).
  • Entry: (board id, player id) → score; tie-break by earliest achievement time.
  • Player: id, display name, region, friends.

3. API

  • POST /scores body: player id, points, match id → accepted; idempotent on match id.
  • GET /boards/{id}/top?n=100 → the top n with names and scores.
  • GET /boards/{id}/rank/{player} → rank, score, and the window of ±5 neighbours.
  • GET /boards/friends/{player} → the player's friends ranked.

4. High-level design

ClientsEdgeServicesDatatop 100POST /scoresrankappendZADDZRANK, ZRANGEGame clientEdge cache(top 100)Load balancerScore serviceRank serviceSnapshot + freezeworkerSorted sets(in memory, replicated)Score log(durable, by player)Score events

A score and a rank:

  1. The score service appends the event to the durable score log keyed by player (the truth), deduplicating on match id, and publishes it.
  2. A consumer applies the event to each relevant sorted set: global, the player's region, and the daily and weekly boards. An update is O(log n).
  3. A rank read asks the sorted set for the player's rank and the range around it (O(log n) each); top 100 is a range read cached at the edge for a second.
  4. At the period boundary the snapshot worker freezes the old board (copies it to durable storage, marks it read-only) and starts a new one.

5. Deep dives

Why a sorted set, and what breaks first?

A database table with an index on score answers top-100 quickly but a player's rank is a count of rows with a greater score, which is O(n) in most engines and becomes a 50-million-row scan per request. An in-memory sorted set (a skip list keyed by score with a hash index by member) gives rank, range and update in O(log n), which is why every leaderboard uses one. What breaks first is the single node: 50,000 updates plus 100,000 reads a second at peak saturates it, and a 3 GB structure is slow to replicate on failover. So partition the global board.

Partitioning a global board while keeping exact top ranks

Partition byRank of a playerTop 100Rebalancing
Player id hash (N shards)sum over N shards of "count greater than score": N callsmerge N top listsnone
Score range (bands)offset of the band plus rank within it: 1 call plus a cached band tablethe top band alonebands shift as scores grow

Choice: score bands. Keep the top band (say the top 10,000 players) on its own replicated node for exact ranks and fast top-100; lower bands hold wider score ranges and may report ranks with a small error from a band-offset table refreshed every second. A score update that crosses a band boundary is a remove from one set and an add to another, done by the consumer in order per player. Band boundaries are recomputed daily from the score distribution and applied at the period rollover so live boards never rebalance.

Real-time updates, idempotency and ordering

The match id makes a score submission idempotent: the durable log rejects a duplicate before anything is published. Events are partitioned by player id so a player's updates are applied in order, and the sorted set holds the latest score per player (or the sum, depending on the game's rule), so a replay after a consumer crash converges to the same state. Because the log is the truth, any sorted set can be rebuilt by replaying the period's events, which is how a lost node or a new region comes up: a 500-million-event day replays in minutes on a few machines.

Friends boards, regional boards and the period rollover

A friends board is tiny and computed on read: fetch the friend ids (hundreds), look up their scores in the global set in one batched call, sort in the service. Regional boards are separate sorted sets fed by the same event, sized at a few million each and unpartitioned. The rollover is the risky moment: scores arriving around midnight must land on the right board, so each event carries its game timestamp and the consumer routes by that, not by arrival time; the old board accepts late events for a grace period before the freeze. Frozen boards are written to object storage as a sorted file and served from a cheap read path, which keeps years of history without memory cost.

Serving reads at 100,000 a second

Top-100 reads dominate and are identical for everyone, so they are computed once a second by the rank service and served from the edge cache; the origin sees a few requests a second for that endpoint. Rank-and-neighbours reads are per player and go to a read replica of the sorted set in the player's region; replicas apply the same event stream, so they lag the primary by a few hundred milliseconds, which is within the one-second freshness goal. The name and avatar for each neighbour come from a profile cache in one batched call. A region that loses its replica serves the last cached top list and shows the player's last known rank with a staleness note, which meets the requirement that boards never disappear.

6. What a strong answer sounds like

  • Explain why rank is the hard query and why a sorted set is O(log n) for it.
  • Size the global board (3 GB, 150,000 ops/s at peak) and show it exceeds one node's headroom, then partition by score band with an exact top band.
  • Keep a durable score log as the truth so the sorted sets are rebuildable, and make submissions idempotent by match id.
  • Handle the period rollover by game time with a grace period.
  • Expect follow-ups on cheating (flagged scores excluded by a filter at apply time), on a player's rank history, and on exact ranks for everyone (a Fenwick tree over score buckets is the classic answer).
  • Common mistake: SELECT COUNT(*) WHERE score > ? per rank request, or a single unreplicated in-memory set as the only copy of the scores.

02Where this question was reported

  • Exponent · 'real-time global leaderboard for a game at the scale of Pokemon Go'
  • Hello Interview · 'Design an Online Game Leaderboard', commonly asked EM
  • PracHub · Chess service with leaderboards
  • PracHub · Retake round; ~10M DAU; interviewer asked for the bottleneck and candidate patched weakest part
  • Blind · Game leaderboard real-time system
  • Prepfully · Candidate reports.