Leaderboard System Design: Real-Time Rankings for Millions of Players
Every competitive game needs a leaderboard. Players want to know where they stand — globally, among friends, and across seasons. What looks like a simple sorted list becomes a serious engineering challenge when millions of concurrent players expect rank updates in real time.
Functional Requirements#
Before diving into architecture, pin down what the leaderboard must do:
- Submit score — Record a player's score after a match or event.
- Get rank — Return a player's current rank and score.
- Top-K — Fetch the top N players (e.g., top 100).
- Nearby ranks — Show players ranked just above and below a given player.
- Time windows — Support daily, weekly, seasonal, and all-time leaderboards.
- Friend leaderboards — Rank a player against their social graph.
Non-Functional Requirements#
- Serve rank queries with latency under 50 ms at the 99th percentile.
- Handle tens of thousands of score updates per second.
- Support hundreds of millions of total players with millions online concurrently.
- Tolerate brief inconsistency (eventual consistency is acceptable for global ranks).
The Naive Approach and Why It Breaks#
A SQL table with ORDER BY score DESC works for small games. Computing rank means counting all rows with a higher score:
SELECT COUNT(*) + 1 AS rank
FROM scores
WHERE score > (SELECT score FROM scores WHERE player_id = ?)
At 100 million rows, this full-table scan takes seconds, not milliseconds. Indexing helps reads but score updates still cause index churn. SQL alone cannot meet the latency or throughput requirements.
Redis Sorted Sets (ZSET) — The Core Building Block#
Redis sorted sets store members with floating-point scores and keep them ordered in a skip list. Every operation that matters is O(log N):
| Operation | Command | Complexity |
|---|---|---|
| Add/update score | ZADD leaderboard score player_id | O(log N) |
| Get rank | ZREVRANK leaderboard player_id | O(log N) |
| Get score | ZSCORE leaderboard player_id | O(1) |
| Top-K | ZREVRANGE leaderboard 0 K-1 WITHSCORES | O(log N + K) |
| Nearby | ZREVRANGE leaderboard rank-5 rank+5 WITHSCORES | O(log N + K) |
With 100 million members, log N is roughly 27. A single Redis instance can handle hundreds of thousands of ZSET operations per second.
Score Encoding Trick#
When two players share the same score, you want the earlier achiever ranked higher. Encode the score as a composite float:
effective_score = actual_score + (1 - timestamp / MAX_TIMESTAMP)
The fractional part is tiny enough to preserve integer score ordering while breaking ties by time.
Scaling Beyond a Single Redis Instance#
A single ZSET tops out around 500 million members before memory becomes a bottleneck (roughly 50-60 GB for 500M entries). Beyond that, partition the leaderboard.
Score-Range Partitioning#
Split the score space into ranges: shard 0 holds scores 0-999, shard 1 holds 1000-1999, and so on. To compute a global rank:
- Identify which shard the player belongs to.
- Get the player's rank within that shard (
ZREVRANK). - Sum the cardinalities (
ZCARD) of all higher shards. - Global rank = sum of higher shard sizes + intra-shard rank + 1.
This requires K calls (one per shard) but each is O(1) for ZCARD and O(log N) for ZREVRANK. With 10-20 shards the fan-out is negligible.
Consistent Hashing for Player Lookup#
Maintain a lightweight lookup (player_id to shard) so score updates route to the correct shard. When a score update moves a player across shard boundaries, remove from the old shard and insert into the new one — both atomic ZSET operations.
Time-Windowed Leaderboards#
Games need daily, weekly, and seasonal boards. Two strategies:
Separate ZSETs Per Window#
Create keys like lb:daily:2026-03-28, lb:weekly:2026-W13, and lb:alltime. On each score event, update all relevant keys. Use Redis TTL to auto-expire old daily and weekly boards.
Rolling Windows with Bucketed Aggregation#
For sliding windows (e.g., "last 7 days"), store per-day ZSETs and compute a union on read:
ZUNIONSTORE lb:rolling:7d 7 lb:day:03-22 lb:day:03-23 ... lb:day:03-28
This is expensive at scale. A better approach: pre-compute the union in a background job every few minutes and serve the cached result. Players tolerate a few minutes of staleness on weekly boards.
Friend Leaderboards#
Fetching ranks among friends cannot use a global ZSET efficiently — you would need to look up each friend individually. Options:
- Small friend lists (under 200): Pipeline
ZSCOREcalls for all friends, sort client-side. Latency stays under 10 ms with pipelining. - Large friend lists: Maintain a per-player friend ZSET updated asynchronously via a fan-out worker. When player A scores, enqueue updates to all friend leaderboards that include A.
The fan-out cost is proportional to average friend count. For a game with an average of 50 friends, a score event triggers 50 additional ZADD calls — easily handled by Redis.
Top-K Queries at Scale#
For the global top 100, avoid hitting the partitioned ZSETs on every request. Instead:
- A background job runs
ZREVRANGE 0 99on each shard every few seconds. - Merge the shard results into a single sorted list.
- Cache the merged top-100 in a simple Redis key or in-memory on the API servers.
- Serve from cache. Staleness of a few seconds is invisible to players.
Anti-Cheat and Score Validation#
A leaderboard is only as trustworthy as its score pipeline.
Server-Authoritative Scoring#
Never trust client-reported scores. The game server computes the score based on match state and submits it directly to the leaderboard service. The client never calls the score-submission endpoint.
Anomaly Detection#
Flag scores that are statistical outliers:
- Score exceeds the 99.9th percentile by more than 3 standard deviations.
- Player's score jumped by more than X% in a single session.
- Score was submitted from a suspicious IP range or device fingerprint.
Flagged scores go into a review queue rather than the live leaderboard.
Rate Limiting#
Cap the number of score submissions per player per time window. A player submitting 1000 scores per minute is either a bot or exploiting a bug.
Persistence and Recovery#
Redis is an in-memory store. Protect against data loss:
- RDB snapshots every 5 minutes for point-in-time recovery.
- AOF (Append Only File) with
everysecfsync for durability within a 1-second window. - Replica sets with automatic failover via Redis Sentinel or Redis Cluster.
- Source-of-truth in a relational database. Write every score event to PostgreSQL (or DynamoDB) asynchronously. If Redis fails catastrophically, rebuild ZSETs from the durable store.
Eventual Consistency Tradeoffs#
Global rank accuracy is not critical to the millisecond. Acceptable tradeoffs:
- Stale top-K caches: Refresh every 5-10 seconds. Players rarely notice.
- Cross-shard rank lag: Shard cardinalities are cached and refreshed periodically. A player's global rank may be off by a handful of positions for a few seconds.
- Async friend board updates: A friend's score may take 1-2 seconds to appear on your friend leaderboard.
For the player's own rank and score, use synchronous reads against their home shard — this must be accurate.
Architecture Diagram (Text)#
Player Device
|
v
Game Server (score validation)
|
v
Score Ingestion Service
|--- async ---> PostgreSQL / DynamoDB (durable store)
|--- sync ----> Redis Cluster (partitioned ZSETs)
|--- async ---> Friend Board Fan-out Workers
|
v
Leaderboard Query Service
|--- top-K cache (in-memory / Redis key)
|--- per-shard rank lookups
|--- friend board reads
|
v
Player Device (rank, top-K, nearby)
Key Metrics to Monitor#
| Metric | Target |
|---|---|
| ZADD latency p99 | under 5 ms |
| ZREVRANK latency p99 | under 5 ms |
| End-to-end rank query p99 | under 50 ms |
| Score ingestion throughput | more than 50K/sec |
| Redis memory per 100M members | 50-60 GB |
| Top-K cache staleness | under 10 seconds |
Summary#
Leaderboard system design is a textbook case of choosing the right data structure. Redis sorted sets give you O(log N) rank operations out of the box. Partition by score range to scale horizontally, use time-keyed ZSETs for windowed boards, fan out to friend boards asynchronously, and cache top-K results aggressively. Pair Redis with a durable store for recovery, validate scores server-side, and accept eventual consistency where it does not hurt the player experience.
Built with Codelit — the system design tool for engineers who think visually.
This is article #205 in the Codelit engineering blog series.
Try it on Codelit
Chaos Mode
Simulate node failures and watch cascading impact across your architecture
Related articles
Try these templates
Uber Real-Time Location System
Handles 5M+ GPS pings per second using H3 hexagonal geospatial indexing.
6 componentsReal-Time Collaborative Editor
Notion-like document editor with real-time collaboration, conflict resolution, and rich media.
9 componentsE-Commerce Checkout System
Production checkout flow with Stripe payments, inventory management, and fraud detection.
11 componentsBuild this architecture
Generate an interactive architecture for Leaderboard System Design in seconds.
Try it in Codelit →
Comments