How to Build a Leaderboard with PHP and Redis

One of biggest draws to Redis for me is the fact that it has more data structure types than just key / value. These additional data structures allow you to do some amazing things while still benefitting from being in-memory. One of the most notable ways to leverage Redis would be when building a leaderboard. Leaderboards are great to gamify existing aspects of a site (number of profile views, number of referred users, et cetera) or to track the stop scores of a game.

Redis becomes super important in scenarios where you need to increment a value. Let’s say you’re tracking how many times a user has posted a status update, if you were to do this in MySQL you could approach it one of two ways. First ever user has a field on your user’s table that tracks the total number of status updates. You would need to run a query like this every time a new status update was posted:

UPDATE users
SET statuses = statuses + 1
WHERE user_id = ?;

It’s terrible but what if you want to grab the top 10 users, you will need to run a query like this:

SELECT user_id, statuses
FROM users
ORDER BY statuses DESC

Not bad with a few hundred records, but as you get more users the query will start to slow down. What if you want to tell a user what their rank is in relation to everyone else? You would need to pull all of the records and then find the user in the record set, there’s no easy way around it.

Second scenario would be to count all of the status records and group them by the user’s ID. I wouldn’t want to be around trying to run that query on a few million records, not to mention trying to find a single user’s rank would be faced with the save caveat of needing to load every record then trying to find where the user is in the set.

Insert Redis’ sorted sets. Sorted sets store a collection of data in order based on the score. Sorted sets up the “z” commands to push and pull data. Let’s use the video game high score leaderboard example, the following will be using the phpredis syntax but can be adapted accordingly to your Redis client of choice.

Every time a user finishes a game you will need to check if the new score is their high score and if it is, set the value in the main leaderboard. You could do this one of two ways, you could keep a separate leaderboard just for the user and check the new score against the high score, or check the value against the user’s existing score on the main leaderboard. For simplicity’s sake, we will use a single master leaderboard:

if ($new_score > $redis->zScore('leaderboard', 'username')) {
    $redis->zAdd('leaderboard', $new_score, 'username');

To help make things a bit easier to follow, I’m going to use textual usernames but I recommend using user ID’s so that you can accommodate for username changes in your own system. If you were working with a value that simply needs to be incremented as an event happens, you could skip the check and simply do $redis->zIncrBy('leaderboard', 1, 'username');. Now that we’re adding records, let’s pull that top 10 list:

$top10 = $redis->zRevRange(
    'leaderboard', 0, 9, array('withscores' => true)

Dijkstra would be proud because Redis’ sets start at 0. This returns the top 10 users (or less if 10 are not available) along with their scores. The data is returned in an array of arrays that contains both the value and the score itself. You are not limited to just the top 10 you could pull the top 100, the full list or paginate it somewhere in between.

Now that we have our top 10 leaderboard, what if you wanted to let a user know their score and rank even though they aren’t part of the top 10? We actually pulled the score in the first example, but I’ll include it here again:

// User's score
$redis->zScore('leaderboard', 'username');

// User's rank
$redis->zRevRank('leaderboard', 'username'); 

One thing to note, zRevRank starts at 0, so you may want to add 1 to it to get a human readable rank. Once you know the rank you could get fancy and pull the user’s “neighbors” on the leaderboard. To do this, you would want to pull some of the user’s in front of the user and some of them after:

$rank = $redis->zRevRank('leaderboard', 'username');

$neighbors = $redis->zRevRange(
    'leaderboard', $rank - 2, $rank + 2, array('withscores' => true)

That example pulls the 2 users ahead and 2 users behind, with the user right in the middle. Another fun trick would be to show the user what percentile they are in relation to the rest of the leaderboard:

$total = $redis->zCard('leaderboard');
$rank  = $redis->zRevRank('leaderboard', 'username') + 1;

if ($rank == 1) {
    echo 'You are the king of the hill!';
} else {
    echo 'You have a higher score than '
        . ((1 - ($rank / $total)) * 100)
        . '% of all gamers!';

I’m sure there’s some other fun things you can do with Redis and leaderboards but this should give you a good start. As mentioned, the syntax used for the examples is for phpredis but it should be pretty easy to port it to Predis or Rediska or even another language if so desired.

Josh Sherman - The Man, The Myth, The Avatar

About Josh

Husband. Father. Pug dad. Musician. Founder of Holiday API, Head of Engineering and Emoji Specialist at Mailshake, and author of the best damn Lorem Ipsum Library for PHP.

If you found this article helpful, please consider buying me a coffee.