Building Real-Time Gaming Leaderboards with Redis: A Practical Guide

Master Spring Ter
4 min readNov 20, 2024

Creating responsive leaderboards for games or competitive applications can be challenging, especially when dealing with millions of users and frequent score updates. Redis, with its powerful sorted sets data structure, makes this task surprisingly simple and incredibly efficient.

Why Redis for Leaderboards?

Redis sorted sets (ZSET) are perfect for leaderboards because they:

  • Automatically maintain elements in sorted order based on scores.
  • Provide O(log N) time complexity for insertions and updates.
  • Allow efficient retrieval of ranked elements using range queries.
  • Support both ascending and descending order rankings.

Prerequisites

  • Redis server installed (version 6.0 or later).
  • Python 3.8+.
  • redis-py library.

Implementation

Let’s build a leaderboard system for a multiplayer game:

import redis
from datetime import datetime
import logging

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LeaderboardException(Exception):
pass

class GameLeaderboard:
def __init__(self, host='localhost', port=6379, db=0):
# Connect to Redis
try:
self.redis_client = redis.Redis(
host=host,
port=port,
db=db,
decode_responses=True
)
# Test connection
self.redis_client.ping()
logger.info("Connected to Redis")
except redis.ConnectionError as e:
logger.error(f"Could not connect to Redis: {e}")
raise LeaderboardException("Redis connection failed")
self.leaderboard_key = "game:leaderboard"

def update_score(self, player_id: str, score: int):
"""Set the player's score in the leaderboard."""
self.redis_client.zadd(self.leaderboard_key, {player_id: score})

def increment_score(self, player_id: str, increment: int):
"""Increment the player's score."""
self.redis_client.zincrby(self.leaderboard_key, increment, player_id)

def get_player_rank(self, player_id: str):
"""Get the player's current rank (1-based)."""
rank = self.redis_client.zrevrank(self.leaderboard_key, player_id)
return rank + 1 if rank is not None else None

def get_top_players(self, count: int = 10):
"""Get the top N players with their scores."""
return self.redis_client.zrevrange(
self.leaderboard_key,
0,
count - 1,
withscores=True
)

def get_player_score(self, player_id: str):
"""Get the player's current score."""
score = self.redis_client.zscore(self.leaderboard_key, player_id)
return float(score) if score is not None else None

def get_players_around(self, player_id: str, window: int = 2):
"""Get players around the specified player."""
rank = self.redis_client.zrevrank(self.leaderboard_key, player_id)
if rank is None:
return []
start = max(0, rank - window)
end = rank + window
return self.redis_client.zrevrange(
self.leaderboard_key,
start,
end,
withscores=True
)

def remove_player(self, player_id: str):
"""Remove a player from the leaderboard."""
self.redis_client.zrem(self.leaderboard_key, player_id)

def get_total_players(self):
"""Get the total number of players in the leaderboard."""
return self.redis_client.zcard(self.leaderboard_key)

def reset_leaderboard(self):
"""Reset the leaderboard."""
self.redis_client.delete(self.leaderboard_key)

Explanation

  • Connection Handling: The constructor now includes error handling for the Redis connection.
  • Increment Score: Added increment_score method using zincrby for cumulative scoring.
  • Additional Methods: Included remove_player, get_total_players, and reset_leaderboard for better management.
  • Type Casting: Ensured that scores are returned as floats for consistency.

Usage Example

Here’s how to use the leaderboard system:

# Initialize leaderboard
leaderboard = GameLeaderboard()

# Add some sample scores
leaderboard.update_score("player1", 1000)
leaderboard.update_score("player2", 2500)
leaderboard.update_score("player3", 1750)
leaderboard.update_score("player4", 3000)

# Increment player1's score
leaderboard.increment_score("player1", 500) # player1's score is now 1500

# Get top 3 players
top_players = leaderboard.get_top_players(3)
print("Top 3 Players:")
for player_id, score in top_players:
rank = leaderboard.get_player_rank(player_id)
print(f"#{rank} - {player_id}: {int(score)}")

# Get players around player2
nearby_players = leaderboard.get_players_around("player2", 1)
print("\nPlayers around player2:")
for player_id, score in nearby_players:
rank = leaderboard.get_player_rank(player_id)
print(f"#{rank} - {player_id}: {int(score)}")

Expected Output

Top 3 Players:
#1 - player4: 3000
#2 - player2: 2500
#3 - player1: 1500

Players around player2:
#1 - player4: 3000
#2 - player2: 2500
#3 - player1: 1500

Performance Considerations

Memory Usage: Redis stores sorted sets in memory. Each entry consumes:

  • Approximately 8 bytes for the score (stored as a double).
  • Memory for the member ID (string length).
  • Overhead for internal data structures (skip lists and hash tables).

Time Complexity:

  • Adding/Updating Scores: O(log N)
  • Getting Rank: O(log N)
  • Retrieving Ranges: O(log N + M), where M is the number of elements returned

Batch Operations: Use Redis pipelines to batch multiple commands for efficiency.

with self.redis_client.pipeline() as pipe:
for player_id, score in score_updates:
pipe.zadd(self.leaderboard_key, {player_id: score})
pipe.execute()

Advanced Features

Time-Based Leaderboards

To implement weekly or monthly leaderboards, include the time period in the key:

def get_time_based_key(self, period='weekly'):
now = datetime.now()
if period == 'weekly':
week_number = now.isocalendar()[1]
return f"game:leaderboard:{now.year}:week:{week_number}"
elif period == 'monthly':
return f"game:leaderboard:{now.year}:month:{now.month}"
else:
return self.leaderboard_key

Update the methods to use the time-based key:

def update_score(self, player_id: str, score: int, period='global'):
"""Set the player's score in the specified leaderboard period."""
leaderboard_key = self.get_time_based_key(period)
self.redis_client.zadd(leaderboard_key, {player_id: score})

Score History Tracking

Track score history using Redis lists:

def record_score_history(self, player_id: str, score: int):
history_key = f"player:{player_id}:history"
timestamp = datetime.now().isoformat()
self.redis_client.rpush(history_key, f"{timestamp}:{score}")
# Optional: Trim history to the last 100 entries
self.redis_client.ltrim(history_key, -100, -1)

Leaderboard Expiration

Set an expiration time on time-based leaderboards to conserve memory:

def update_score(self, player_id: str, score: int, period='weekly'):
leaderboard_key = self.get_time_based_key(period)
self.redis_client.zadd(leaderboard_key, {player_id: score})
# Set expiration of 30 days
self.redis_client.expire(leaderboard_key, 2592000)

Error Handling and Production Considerations

Implement proper error handling and connection management:

def safe_update_score(self, player_id: str, score: int):
try:
self.update_score(player_id, score)
except redis.RedisError as e:
logger.error(f"Redis error updating score for {player_id}: {e}")
raise LeaderboardException("Failed to update score")

Consider using a connection pool and handling reconnections:

self.redis_client = redis.Redis(
host=host,
port=port,
db=db,
decode_responses=True,
socket_timeout=5,
socket_connect_timeout=5
)

Monitoring and Maintenance

  • Memory Usage: Monitor Redis memory usage using INFO memory and MEMORY STATS.
  • Persistence: Configure Redis persistence (RDB snapshots or AOF) based on your needs.
  • Backup Strategy: Regularly back up Redis data if losing the leaderboard data is unacceptable.
  • Security: Use Redis authentication and firewall rules to secure your Redis server.

Conclusion

Redis sorted sets provide an elegant and efficient solution for implementing real-time leaderboards. With automatic sorting, efficient updates, and fast range queries, Redis is ideal for gaming applications, competitive platforms, or any system requiring ranked data.

This implementation can handle millions of users while maintaining sub-millisecond response times. By following the patterns shown here, you can create robust, scalable leaderboard systems that grow with your application.

Remember to monitor Redis memory usage, handle exceptions gracefully, and implement appropriate backup and security strategies in production environments.

Note: Always test your implementation thoroughly, especially when integrating into a production environment. Consider edge cases, concurrency issues, and data persistence requirements.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Master Spring Ter
Master Spring Ter

Written by Master Spring Ter

https://chatgpt.com/g/g-dHq8Bxx92-master-spring-ter Specialized ChatGPT expert in Spring Boot, offering insights and guidance for developers.

No responses yet

Write a response