Back to blog
Redis leaderboard system showing ranked players with scores on a futuristic digital screen
System Design

Build a Redis Leaderboard for 100 Million Players

May 27, 2026 10 min read Avinash Tyagi
redis sorted set redis leaderboard gaming leaderboard leaderboard system design redis zadd sorted set data structure real-time leaderboard redis data types system design interview back of envelope estimation

I've been breaking down system design concepts I found hard to understand or struggled to apply in interviews. This one came from a mock interview that went sideways when I tried to explain leaderboard system design for 100 million concurrent players.

The problem that broke my SQL brain

You're building a gaming leaderboard. Players finish matches every second. Hundreds of thousands of score updates stream in. Your users expect the leaderboard to update in real time, showing their exact rank among millions of players.

My first instinct was PostgreSQL. Throw user scores in a table, add an index, run SELECT * FROM scores ORDER BY score DESC LIMIT 10. Done, right?

Not even close. With 100 million players and thousands of score updates per second, every write triggers a re-sort. Every read triggers a full index scan. The database spends all its CPU maintaining sort order instead of answering queries. The leaderboard freezes. Players rage-quit. Your on-call engineer gets paged at 3am.

The interviewer watched me spiral and asked: "What if the data structure sorted itself on write instead of read?"

That's when I learned about Redis sorted sets.

What is a Redis sorted set

A Redis sorted set stores unique members, each paired with a floating-point score. Redis keeps the set ordered by score at all times. When you add a new member or update a score, Redis places it in the correct position right away. No background job. No periodic re-sort. It just stays sorted.

Under the hood, Redis uses two data structures together: a hash table and a skip list. The hash table maps members to scores for O(1) lookups. The skip list maintains sorted order for O(log N) insertions and range queries.

Think of a skip list like a linked list with express lanes. A regular linked list requires you to walk through every node. A skip list adds multiple layers of shortcuts. The top layer has very few nodes and lets you jump ahead fast. Each layer below has more nodes and finer detail. Finding the right position takes O(log N) hops instead of O(N).

This dual structure is what makes sorted sets effective for a redis leaderboard. You get constant-time score lookups AND logarithmic-time ranked queries in one data structure.

Why not a balanced binary tree like a Red-Black tree? Skip lists are simpler to build, and the code is more readable. Redis creator Salvatore Sanfilippo chose them partly for that reason. In a system that values reliability, simpler code wins.

Architecture diagram of a Redis leaderboard system with game servers, API gateway, PostgreSQL, and Redis sorted set
Redis leaderboard system architecture: dual-write to PostgreSQL and Redis sorted set

Five Redis commands for every leaderboard

You don't need the full Redis command reference. Five commands handle everything in a redis leaderboard:

ZADD inserts a player with their score. If the player already exists, redis zadd updates their score in one step. Time complexity: O(log N). This is your write path.

bash
ZADD leaderboard 2500 "player_42"

ZINCRBY adds to a player's score without a read-modify-write cycle. If two game servers submit user scores for the same player at the same millisecond, Redis handles it correctly because it's single-threaded.

bash
ZINCRBY leaderboard 100 "player_42"

ZREVRANGE returns the top N players, highest scores first. The 0 9 range gives you the top 10. Time complexity: O(log N + M) where M is the count of entries returned.

bash
ZREVRANGE leaderboard 0 9 WITHSCORES

ZREVRANK tells a player their exact rank. "You are #4,523 out of 100 million players." This is O(log N). Try doing that with a SQL COUNT(*) on 100 million rows.

bash
ZREVRANK leaderboard "player_42"

ZRANGEBYSCORE returns all players within a score range. Useful for showing players near your rank or building tiered leaderboards (bronze, silver, gold).

bash
ZRANGEBYSCORE leaderboard 2000 3000 WITHSCORES

Why SQL breaks and Redis doesn't

The core difference comes down to when the sorting happens.

SQL sorts on read. You store rows in insertion order, and when someone asks for the top 10, the database walks the index, touches disk, acquires locks, and competes with concurrent writes. Under heavy load, every leaderboard page view triggers real work.

Redis sorts on write. When you call redis zadd, Redis finds the correct position in the skip list and inserts there. The sorted order is a property of the data structure itself. When someone asks for the top 10, Redis reads 10 nodes from the skip list. Almost zero work.

For a redis leaderboard where reads far outnumber writes (millions checking, thousands submitting user scores), you want the heavy work on the write path, not the read path.

The rank lookup is the killer. SELECT COUNT(*) FROM scores WHERE score > player_score on 100 million rows is brutal. Redis gives you the same answer with a single ZREVRANK call in microseconds.

Back-of-envelope math: can one Redis instance handle this?

Before designing the full system, I wanted to verify that Redis can handle the scale.

Memory: Each sorted set member uses roughly 80 bytes of overhead plus the member string and score. With 100 million players and 20-byte player IDs: (80 + 20 + 8) bytes times 100M equals roughly 10.8 GB. A server with 16 GB of RAM handles this well.

Write throughput: A single Redis instance handles 100,000+ redis zadd operations per second. If your game generates 10,000 score updates per second, you're using 10% of capacity.

Read throughput: ZREVRANGE for the top 10 takes about 0.1ms. ZREVRANK takes about 0.1ms. Even at 50,000 reads per second, Redis handles it easily.

Network: Each ZADD command is about 50 bytes. At 10,000 writes/second, that's 500 KB/s of inbound traffic. Tiny on a modern network.

The math checks out. One Redis instance can run a redis leaderboard for 100 million registered players with tens of thousands of concurrent score updates.

Leaderboard system design for production

Knowing Redis sorted sets exist is one thing. Building a real leaderboard system design is another.

The write path

Game servers submit user scores to an API gateway. The gateway writes to two places: PostgreSQL for storage, and Redis for the live leaderboard. The database is your source of truth. Redis is your ranking engine.

Why both? Redis is in-memory. If it crashes, you lose data unless you have persistence set up (RDB snapshots or AOF logging). Even with persistence, recent writes could be lost. The database gives you a recovery path.

write_path.pypython
async def update_score(player_id: str, score: int):
    await db.execute(
        "INSERT INTO scores (player_id, score) VALUES ($1, $2) "
        "ON CONFLICT (player_id) DO UPDATE SET score = $2",
        player_id, score
    )
    await redis.zadd("leaderboard", {player_id: score})

The read path

The leaderboard page reads only from Redis. No database queries. Three endpoints cover everything:

read_path.pypython
async def get_top_players(n: int = 10):
    return await redis.zrevrange("leaderboard", 0, n - 1, withscores=True)

async def get_player_rank(player_id: str):
    rank = await redis.zrevrank("leaderboard", player_id)
    score = await redis.zscore("leaderboard", player_id)
    return {"rank": rank + 1 if rank is not None else None, "score": score}

async def get_nearby_players(player_id: str, window: int = 5):
    rank = await redis.zrevrank("leaderboard", player_id)
    if rank is None:
        return []
    start = max(0, rank - window)
    return await redis.zrevrange("leaderboard", start, rank + window, withscores=True)

No JOIN, no subquery, no aggregation. Each function is a single Redis command.

Handling ties

When two players have the same score, Redis sorts them by member name. If you need tie-breaking by timestamp (earlier score wins), encode the timestamp into the score. The main score dominates, but ties break in favor of whoever reached that score first.

Scaling beyond one instance

At massive scale, two approaches work:

Shard by region or game mode. Maintain separate sorted sets per shard. Each shard fits on one Redis node. Build a global view by merging shards with ZUNIONSTORE.

Read replicas. Write to the primary, read from replicas. Leaderboard data can be a few seconds stale without hurting user experience. Nobody notices #4,523 vs #4,525.

Periodic snapshots

For weekly leaderboards or season rankings, create time-bounded sorted sets. At the start of each week, create a new key. User scores go to both the global and the period key. At the end of the period, the key becomes read-only.

periodic_snapshots.pypython
async def update_score_with_period(player_id: str, score: int):
    week = time.strftime("%Y-w%W")
    pipe = redis.pipeline()
    pipe.zadd("leaderboard:global", {player_id: score})
    pipe.zadd(f"leaderboard:{week}", {player_id: score})
    await pipe.execute()

Pipelining batches both writes into a single round-trip, cutting latency on the hot path.

Common mistakes I made

Using ZRANGE instead of ZREVRANGE. ZRANGE returns lowest scores first. For a leaderboard where higher is better, you want ZREVRANGE. I spent 20 minutes debugging why the "top 10" were all the lowest-scoring players.

Forgetting about memory. Each member takes 80-100 bytes of overhead. For 100 million players, that's 10-12 GB. Fine for a dedicated instance, but don't run your redis leaderboard on the same instance that handles session caching.

Skipping TTL for inactive players. If a player hasn't played in 6 months, they still use memory. Run a background job to ZREM inactive players. Keep their user scores in the database so they can come back.

Ignoring persistence config. Redis can lose recent writes on restart. If you can rebuild from the database, that's fine. If Redis is your only ranking store, enable AOF with appendfsync everysec at minimum.

When sorted sets aren't enough

Sorted sets work great for single-dimension rankings. But they have limits:

Multi-dimension rankings (score, then win rate, then games played) need a composite score or a separate ranking service.

Historical leaderboards need separate sorted sets per time period, which multiplies memory usage.

Friend-only leaderboards can't use a global sorted set well. You'd need per-user sorted sets or fetch scores for a friend list with ZMSCORE and sort on the client.

For most gaming use cases though, one Redis sorted set per game mode gets you far. Millions of players, sub-millisecond queries, thousands of writes per second, all on one instance.

What to explore next

If this clicked for you, here are natural next steps:

  1. Redis Streams for processing score events in order. Useful for rolling averages, cheating detection, or replaying events after a crash.
  2. Consistent hashing for spreading leaderboard shards across Redis nodes without hotspots.
  3. Rate limiting with sorted sets. Same data structure, different use case. Store request timestamps per user and use ZRANGEBYSCORE with a sliding time window.
  4. Edge caching for leaderboard pages. The top 10 rarely changes second-to-second. A short TTL (5-10 seconds) at the CDN edge cuts most Redis reads.

The bottom line

The leaderboard problem looks simple. Store scores, sort them, show the top 10. But at scale, your choice of data types and data structures decides whether your system handles 100 players or 100 million. Redis sorted sets solve this because they were designed for ranked data. The sorting happens on every write, making reads almost free.

The pattern applies beyond gaming. Trending feeds, analytics dashboards, priority queues, rate limiters. Anywhere you need "give me the top N, updated in real time," sorted sets are the right tool. I've been working through leaderboard system design problems on Levelop and this question keeps coming back in different forms.

Frequently asked questions

What is a Redis sorted set and how does it work?

A Redis sorted set is a data structure that stores unique members, each paired with a numeric score. Redis keeps all members sorted by score at all times using a skip list and hash table internally. Insertions and score updates are O(log N), and fetching ranked ranges is also O(log N + M) where M is the number of results returned.

Why is Redis better than SQL for leaderboards?

SQL databases sort data on read, which means every leaderboard page view triggers an expensive index scan. Redis sorts on write using a sorted set, so reads are nearly free. Redis also provides O(log N) rank lookups via ZREVRANK, while SQL requires an O(N) COUNT query to determine a player's position among millions.

How much memory does a Redis sorted set need for 100 million players?

Each sorted set member uses roughly 80-100 bytes of overhead plus the member string size. For 100 million players with 20-byte player IDs, expect about 10-12 GB of RAM usage. A dedicated Redis instance with 16 GB handles this comfortably.

Can a single Redis instance handle a leaderboard for 100 million players?

Yes. A single Redis instance on modern hardware handles 100,000+ ZADD operations per second and sub-millisecond ZREVRANGE and ZREVRANK queries. For most gaming leaderboards with tens of thousands of concurrent score updates, one instance is sufficient.

How do you handle leaderboard ties in Redis?

By default, Redis sorts members with equal scores in lexicographic order by member name. If you need tie-breaking by timestamp, you can encode the timestamp into the score using a formula like score * 10^10 + (MAX_TIMESTAMP - actual_timestamp), so the main score dominates but ties break by who scored first.

Keep reading

System Design

Caching Strategies for System Design Interviews

Four caching strategies, four failure modes. Learn cache-aside, read-through, write-through, and write-behind — when to use each and what breaks when things go wrong.

Read article
System Design

4 Caching Strategies for System Design

Master cache-aside, read-through, write-through, and write-behind patterns. Learn when each strategy wins, what breaks, and how to explain the tradeoffs in system design interviews.

Read article
System Design

3 System Design Patterns Every Engineer Should Know

Master three essential system design patterns — Layered Architecture, Pub/Sub Messaging, and CQRS — with practical examples and guidance on when to use each.

Read article