Real-Time Leaderboard with Redis Sorted Sets
June 9, 2026
6 min read
Leaderboards are deceptively hard. Sorting millions of scores on every request is expensive, and keeping rankings fresh in real time strains any SQL database. Redis Sorted Sets solve this elegantly — every operation that updates a score also updates the rank, in O(log N) time.
What Are Redis Sorted Sets?
A sorted set is a collection of unique members, each associated with a floating-point score. Redis keeps members sorted by score at all times. You can retrieve members by rank (position) or by score range, and update scores atomically.
Every leaderboard operation maps directly to a sorted set command:
- Add/update score →
ZADD - Get player rank →
ZREVRANK - Get top-N players →
ZREVRANGE - Increment score →
ZINCRBY - Get score →
ZSCORE
Basic Leaderboard Operations
# Add players with scores
ZADD leaderboard:global 5000 "alice"
ZADD leaderboard:global 8200 "bob"
ZADD leaderboard:global 7100 "carol"
ZADD leaderboard:global 9500 "dave"
# Get top 3 players (highest scores first) with scores
ZREVRANGE leaderboard:global 0 2 WITHSCORES
# 1) "dave" 2) "9500"
# 3) "carol" 4) "7100"
# 5) "bob" 6) "8200"
# Get alice's rank (0-indexed, lower is better rank)
ZREVRANK leaderboard:global "alice" # returns 3 (4th place)
# Get alice's score
ZSCORE leaderboard:global "alice" # returns "5000"
# Add 500 points to alice's score (atomic increment)
ZINCRBY leaderboard:global 500 "alice"
# Total number of players
ZCARD leaderboard:global
Python Implementation
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
LEADERBOARD = "leaderboard:global"
def add_score(player, score):
r.zadd(LEADERBOARD, {player: score})
def increment_score(player, points):
return r.zincrby(LEADERBOARD, points, player)
def get_rank(player):
rank = r.zrevrank(LEADERBOARD, player)
return rank + 1 if rank is not None else None # 1-indexed
def get_top(n=10):
top = r.zrevrange(LEADERBOARD, 0, n - 1, withscores=True)
return [{"rank": i + 1, "player": p, "score": int(s)}
for i, (p, s) in enumerate(top)]
def get_around_player(player, radius=2):
rank = r.zrevrank(LEADERBOARD, player)
if rank is None:
return []
start = max(0, rank - radius)
end = rank + radius
entries = r.zrevrange(LEADERBOARD, start, end, withscores=True)
return [{"rank": start + i + 1, "player": p, "score": int(s)}
for i, (p, s) in enumerate(entries)]
Time-Windowed Leaderboards
Daily, weekly, and all-time leaderboards are common requirements. Use separate keys per time window and let them expire automatically.
from datetime import datetime
def get_leaderboard_key(period='global'):
today = datetime.utcnow()
if period == 'daily':
return f"leaderboard:{today.strftime('%Y-%m-%d')}"
elif period == 'weekly':
week = today.isocalendar()[1]
return f"leaderboard:{today.year}:w{week}"
return "leaderboard:global"
def record_score(player, points, period='global'):
key = get_leaderboard_key(period)
r.zincrby(key, points, player)
# Auto-expire daily boards after 2 days
if period == 'daily':
r.expire(key, 172800) # 48 hours
elif period == 'weekly':
r.expire(key, 604800 * 2) # 2 weeks
Pagination
def get_leaderboard_page(page=1, page_size=20):
start = (page - 1) * page_size
end = start + page_size - 1
entries = r.zrevrange(LEADERBOARD, start, end, withscores=True)
return [{"rank": start + i + 1, "player": p, "score": int(s)}
for i, (p, s) in enumerate(entries)]
Key Takeaways
- Sorted sets maintain order automatically — no sorting needed at read time
- ZINCRBY atomically increments scores — safe for concurrent score updates
- ZREVRANK gives rank in O(log N) — instant regardless of leaderboard size
- Separate keys per time window enable daily/weekly/all-time boards with automatic cleanup via EXPIRE
- A leaderboard with 10 million players uses roughly 800MB of RAM — very manageable