Lesson 54 of 105 12 minFlagship

System Design: Designing a Real-time Gaming Leaderboard (Massive Scale)

How does Fortnite or Candy Crush update leaderboards for millions of users instantly? A technical deep dive into Redis Sorted Sets, Sharding, and Caching.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

Key Takeaways

  • **Redis Sorted Sets:** Leveraging Skip Lists and Hash Maps to achieve logarithmic O(log N) time complexity for rank updates and range retrievals.
  • **Write-Behind Cache:** Buffering high-frequency score submissions using Kafka to guarantee durability in Postgres without bottlenecking memory.
  • **Scale Capacity Sizing:** Estimating memory footprints and partitioning active sets by geographical region or seasonal keys.
Recommended Prerequisites
System Design Interview Framework

Premium outcome

From vague architecture answers to staff-level trade-off thinking.

Backend engineers preparing for senior, staff, and architecture rounds.

What you unlock

  • A reusable system design answer framework for ambiguous prompts
  • Clear language for consistency, scaling, and reliability trade-offs
  • Case-study depth across feeds, payments, storage, and messaging systems

Mental Model

A real-time gaming leaderboard is a performance-critical system where score updates and rank calculations must be resolved instantly for millions of active users. Standard relational databases fail at high-throughput ranking due to the high I/O cost of B-Tree indexes under frequent write updates. To achieve sub-millisecond updates and retrievals, the system relies on Redis Sorted Sets (ZSETs)—which combine a Skip List and a Hash Map. By coupling this in-memory speed with a Kafka-backed write-behind queue for transactional database backups, we build a highly available, self-healing, and low-latency global leader system.


Requirements and System Goals

To architect a global-scale leaderboard for massive games (like Fortnite or Candy Crush), we must establish quantitative scaling metrics.

1. Functional Requirements

  • Real-time Score Submission: Players submit scores after completing a level or round, updating their position immediately.
  • Top-K Leaderboard Retrieval: Fetch the top 100 players globally in less than 50ms.
  • Relative Rank Retrieval: Retrieve a specific player's exact rank, their current score, and the 5 players immediately above and below them on the scoreboard.

2. Non-Functional Requirements & Scale Budgets

  • Ultra-High Write Throughput: Support up to 50,000 concurrent score submissions per second during peak gaming events.
  • Low Latency Read SLA: Deliver top-K and relative ranks to clients with a p99 latency budget less than 20ms.
  • Global Scalability: Scale to 10 Million active players per gaming season while keeping memory utilization optimized.
  • High Availability & Fault Tolerance: Recover from total in-memory node crashes without losing historical player score transactions.

API Interfaces and Service Contracts

We define standard REST and JSON contracts for the leaderboard service cluster.

1. Submit Player Score API Contract

This endpoint is called by the game client when a match finishes to register a new score.

POST /api/v1/leaderboards/{leaderboard_id}/scores

Request Payload:

{
  "player_id": "usr_9a8b7c6d5e",
  "score_delta": 450,
  "game_session_id": "sess_8833912a-4422-4911-88aa-bbccddeeff11",
  "submitted_at": "2026-05-31T13:51:00Z"
}

Response Payload (200 OK):

{
  "status": "score_updated",
  "leaderboard_id": "global_season_5",
  "player_id": "usr_9a8b7c6d5e",
  "new_score": 12500,
  "new_rank": 4821,
  "updated_at": "2026-05-31T13:51:01Z"
}

2. Get Relative Rank API Contract

Retrieves the target player's exact global rank along with a contextual block of surrounding competitors.

GET /api/v1/leaderboards/{leaderboard_id}/ranks?player_id=usr_9a8b7c6d5e&context_range=5

Response Payload (200 OK):

{
  "leaderboard_id": "global_season_5",
  "target_player": {
    "player_id": "usr_9a8b7c6d5e",
    "rank": 4821,
    "score": 12500
  },
  "context": [
    { "player_id": "usr_top_4819", "rank": 4819, "score": 12520 },
    { "player_id": "usr_top_4820", "rank": 4820, "score": 12505 },
    { "player_id": "usr_9a8b7c6d5e", "rank": 4821, "score": 12500 },
    { "player_id": "usr_bot_4822", "rank": 4822, "score": 12495 },
    { "player_id": "usr_bot_4823", "rank": 4823, "score": 12480 }
  ]
}

High-Level Design and Visualizations

To scale score ingestion independently from real-time rank retrievals, we design a multi-layered decoupled infrastructure.

1. Global Leaderboard Architecture Layout

The following diagram illustrates how incoming score writes are buffered via a message queue to guarantee persistence in a relational database, while reads are served directly from a sharded, high-performance Redis Sorted Set cluster.

graph TD
    subgraph Client Layer
        Player[Game Clients] -->|1. Submit Score / Fetch Rank| LB[Global Load Balancer]
    end

    subgraph Service Layer
        LB -->|2. Route Requests| APIGateway[API Gateway]
        APIGateway -->|3. Read Rank / Top-K| ReadSvc[Stateless Leaderboard Read Service]
        APIGateway -->|3. Post Score| WriteSvc[Stateless Leaderboard Ingestion Service]
    end

    subgraph Messaging Buffer
        WriteSvc -->|4. Publish Ingestion Event| Kafka[Kafka Event Pipeline]
    end

    subgraph Cache & Ranking Tier
        ReadSvc -->|5. Instant Rank Query| RedisCluster[Redis Sharded ZSET Cluster]
        Consumer[Kafka Ingestion Consumer] -->|6. Fast ZADD Updates| RedisCluster
    end

    subgraph Durable Core Database
        Consumer -->|7. Write-Behind Batch Ingest| Postgres[(PostgreSQL Core Cluster)]
    end

2. High-Frequency Score Ingestion and Read Paths

The sequence diagram below displays the end-to-end processing of a player's score update and the parallel retrieval of the top leaderboard.

sequenceDiagram
    autonumber
    participant Client as Game Client
    participant API as Ingestion Service
    participant Queue as Kafka Cluster
    participant Consumer as Ingestion Consumer
    participant Redis as Redis Cluster (ZSET)
    participant DB as PostgreSQL Store

    Note over Client, DB: Ingestion Path (Asynchronous, High Throughput)
    Client->>API: POST /api/v1/leaderboards/season_5/scores (score delta)
    API->>Queue: Publish event (player_id, score_delta)
    API-->>Client: 200 OK (Queued for rank updates)
    
    Queue->>Consumer: Pull event batch
    Consumer->>Redis: ZINCRBY leaderboard:season_5 score_delta player_id
    Consumer->>DB: INSERT INTO score_transactions ... ON CONFLICT UPDATE
    
    Note over Client, Redis: Retrieval Path (Real-time, Low Latency)
    Client->>API: GET /api/v1/leaderboards/season_5/ranks?player_id=123
    API->>Redis: ZREVRANK leaderboard:season_5 123
    Redis-->>API: Return Rank (e.g. 4821)
    API->>Redis: ZREVRANGE leaderboard:season_5 4816 4826 WITHSCORES
    Redis-->>API: Return Ranks 4816 to 4826
    API-->>Client: Return relative rank page

Low-Level Design and Schema Strategies

To support historical audits, the primary source of truth is a transactional database. Ranks are then calculated and cached in memory using optimized Redis commands.

1. Relational Backup Database Schema (PostgreSQL)

We define the database structures used to store every score transaction and aggregate player summaries for disaster recovery.

-- Core Player Summary Scores
CREATE TABLE player_leaderboards (
    player_id VARCHAR(64) NOT NULL,
    leaderboard_id VARCHAR(64) NOT NULL,
    total_score BIGINT NOT NULL DEFAULT 0,
    matches_played INT DEFAULT 0,
    last_updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    PRIMARY KEY (player_id, leaderboard_id)
);

-- Audit log of individual score submissions
CREATE TABLE score_ingestion_log (
    transaction_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    player_id VARCHAR(64) NOT NULL,
    leaderboard_id VARCHAR(64) NOT NULL,
    score_delta INT NOT NULL,
    game_session_id VARCHAR(64) NOT NULL,
    processed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Fast indexes for historical lookups and database aggregations
CREATE INDEX idx_leaderboard_score ON player_leaderboards(leaderboard_id, total_score DESC);
CREATE INDEX idx_audit_player ON score_ingestion_log(player_id, processed_at);

2. Redis Sorted Set Commands and In-Memory Mapping

The Redis Sorted Set (ZSET) structures represent the leaderboard in-memory using a combination of a hash table (player_id mapped to score) and a skip list (scores sorted ascending).

Command Name Usage Description Complexity
ZADD key score player ZADD leaderboard:season_5 12500 usr_9a8b Adds/overwrites player score in the leaderboard. $O(\log N)$
ZINCRBY key increment player ZINCRBY leaderboard:season_5 450 usr_9a8b Increments player score (perfect for accumulative stats). $O(\log N)$
ZREVRANK key player ZREVRANK leaderboard:season_5 usr_9a8b Retrieves 0-indexed rank of player (sorted high to low). $O(\log N)$
ZREVRANGE key start stop ZREVRANGE leaderboard:season_5 0 99 Fetches Top-100 players with highest scores. $O(\log N + M)$
ZSCORE key player ZSCORE leaderboard:season_5 usr_9a8b Fetches score of a specific player. $O(1)$

Scaling and Operational Challenges

1. Redis ZSET Capacity Sizing Calculations

To prevent out-of-memory crashes on our caching tier, we must perform precise back-of-the-envelope capacity estimations.

  • Sizing Variables:
    • Total unique active players ($N$): 10,000,000 (10M).
    • Score value representation: 8 bytes (using 64-bit integers).
    • Player ID string representation: usr_ + UUID (e.g. usr_9a8b7c6d5e), averaging 32 bytes.
    • Sorted Set Node overhead in Redis: Every element in a ZSET consists of a dictionary node and a skip list node. The skip list pointer overhead, memory page fragmentation, and internal pointers average 128 bytes of metadata overhead per node.
  • The Sizing Formula: $$\text{Memory}{\text{base}} = N \times (\text{Size}{\text{score}} + \text{Size}{\text{player_id}} + \text{Metadata}{\text{overhead}})$$ $$\text{Memory}{\text{base}} = 10,000,000 \times (8 \text{ bytes} + 32 \text{ bytes} + 128 \text{ bytes})$$ $$\text{Memory}{\text{base}} = 10,000,000 \times 168 \text{ bytes} = 1,680,000,000 \text{ bytes} \approx 1.68 \text{ GB}$$
  • Sizing Safety Multiplier: To account for replication buffers, client output buffers, and Redis runtime heap overhead, we apply a $1.5\times$ safety headroom factor: $$\text{Memory}_{\text{total}} = 1.68 \text{ GB} \times 1.5 \approx 2.52 \text{ GB}$$
  • Operational Insight: While 2.52 GB is small enough to easily fit on a single standard Redis node, the CPU cycles required to execute frequent ZINCRBY writes (50,000 writes/sec) will saturate a single-threaded Redis process. Thus, we must shard the leaderboard.

2. Global Leaderboard Partition Sharding Key Strategies

Because sorted sets are stored on single Redis nodes, you cannot easily perform a standard hash partition on player_id because calculating a global rank would require checking all nodes. We utilize two main scaling partition models:

  • Model A: Seasonal and Geographical Partitioning:
    • Instead of a single monolithic key, we split traffic by Region and Game Mode (e.g., leaderboard:EU:season_5:solo, leaderboard:US:season_5:solo).
    • The Benefit: Ranks remain isolated within each player's local region, preventing the need for complex cross-node sorting.
  • Model B: Score-Range Sharding:
    • If a true, absolute global rank is required, we shard the Redis cluster by score ranges (e.g., Shard 1 holds scores 0 to 5,000; Shard 2 holds scores 5,001 to 10,000, and so on).
    • Retrieval Math: To find the exact global rank of a player in Shard 2, we query Shard 3 and all higher shards for their total element counts (ZCARD), and then sum those counts with the player's local rank within Shard 2, reducing global network latency.

Leaderboard Architecture Trade-offs

Every system design choice involves balancing consistency, processing complexity, and latency.

Architectural Path Exact Global Ranks (ZSET Skip List) Approximate Global Ranks (HyperLogLog / Count-Min Sketches)
Data Structure Complexity Medium (ZSET commands are native and fast) Low (Maintains small, probabilistic array hashes)
Memory Consumption High (Approximately 168 bytes per player) Ultra-Low (Fits 10M players in less than 12KB of memory)
Write and Update Latency $O(\log N)$ updates $O(1)$ constant-time hashing
Ranking Accuracy 100% Exact ranking results Approximate results (1-2% margin of error)
Best Use Case Competitive esports leagues where precise rank is mandatory. General gaming apps showing "You are in the top 5%" feeds.

Failure Modes and Fault Tolerance Strategies

1. Redis Memory Exhaustion (Eviction Safeguards)

If Redis memory usage approaches its limits, a standard Redis configuration might run its eviction policy (e.g. volatile-lru or allkeys-lru), silently deleting random player keys to free up space.

  • The Safe Solution: We must configure the Redis instance with a strict maxmemory-policy noeviction policy.
  • Under noeviction, if Redis runs out of memory, it blocks new writes and returns memory errors rather than deleting active leaderboard data.
  • The ingestion queue (Kafka) continues to buffer incoming scores safely, preventing data loss.
  • System alerts trigger instantly, allowing automated scripts or staff operators to provision additional memory or execute seasonal key archiving.

2. Disrupted Cache Recovery (Cold Start Cache Hydration)

If a primary Redis node suffers a hardware failure, it reboots with an empty memory cache, triggering massive downstream query spikes to our core PostgreSQL database.

  • The Hydration Strategy: We utilize Cold Start Cache Hydration via our Kafka event stream.
  • When a node reboots, the leaderboard read services are set to "Maintenance Mode" for the affected keys.
  • We spawn a background hydration script that reads player aggregates from PostgreSQL in chunks of 50,000 rows.
  • The script issues parallel pipelined ZADD commands to Redis.
  • Once the cache size matches the database counts, we switch the keys back to active read mode, preventing relational database thread starvation.

Staff Engineer Perspective


Production Readiness Checklist

Before launching a global gaming leaderboard to millions of players, verify:

  • Noeviction Active: Confirm that the active Redis keyspaces use maxmemory-policy noeviction.
  • Pipelined Ingestion: Ingest consumers pool writes and execute ZADD using pipelining in batches of 500 to save connection network round trips.
  • Top-K Page Cache: Cache the global Top-100 player list in an application-level memory cache (e.g., Guava) for 2 seconds to reduce redundant read reads on Redis.
  • Kafka Retention Limits: Set Kafka transaction logs retention to a minimum of 24 hours to survive long database outages during resyncs.


Verbal Script

Interviewer: "How would you design a real-time gaming leaderboard that supports millions of active players, handles a write load of 50,000 score submissions per second, and returns relative rankings instantly?"

Candidate: "To design a real-time gaming leaderboard at this massive scale, I would avoid relational databases on the hot path. Standard B-Tree indexing incurs high random I/O write locks when handling frequent updates. Instead, I would build an event-driven architecture using Redis Sorted Sets (ZSETs) for our memory ranking engine and Kafka for write buffering.

Our ingestion services receive score updates from game clients. Instead of writing directly to the database, the service publishes a lightweight score ingestion event to a Kafka cluster. This decouples the ingestion layer, permitting our system to easily absorb spike write workloads up to 50,000 writes/sec.

A pool of event-driven consumers pulls these score batches. The consumers perform two tasks. First, they update the sharded Redis Sorted Set cluster using the ZINCRBY or ZADD command. Second, they write the score transaction to a persistent PostgreSQL database in batches, guaranteeing durability.

To calculate and return a player's rank and relative scores instantly in less than 20ms, the stateless read services query Redis directly. A sorted set internally coordinates a Skip List and a Hash Map. The Hash Map provides $O(1)$ player score lookup, and the Skip List enables $O(\log N)$ logarithmic rank updates, relative positioning, and range queries.

To fetch a relative rank, the service runs ZREVRANK to determine the player's exact rank. It then issues a ZREVRANGE query specifying a small offset window—like 5 ranks above and below—returning the customized relative leaderboard array.

Finally, to manage memory boundaries and prevent catastrophic data eviction, I would configure Redis with a strict noeviction policy. If a Redis node fails, we treat it as a volatile cache and rebuild its sorted state using background cold-start scripts that query player summary records from our PostgreSQL source of truth, guaranteeing high availability and absolute reliability."

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.