Skip to main content
Redis · lesson 4 of 14
Redis beginner Lesson 4 of 14

Sorted Sets: Building a Leaderboard, and the Ascending-Order Trap

ZRANGE returns lowest score first by default — a real surprise the first time you build a leaderboard and see the worst player listed first. ZREVRANGE, ZRANK, and range queries fix it.

By SkillByExample · Published

redis sorted-sets leaderboard zrange

A sorted set keeps every member ranked by a score automatically — the surprise is which direction “ranked” means by default.

Building a leaderboard

docker exec redis-lesson redis-cli ZADD leaderboard 100 alice 250 bob 175 carol
docker exec redis-lesson redis-cli ZRANGE leaderboard 0 -1 WITHSCORES
3
alice
100
carol
175
bob
250

ZADD returns 3 — three new members added. ZRANGE ... WITHSCORES lists everyone… in ascending order. Alice has the lowest score and is listed first — for a feature literally called a leaderboard, that’s backwards from what most people expect on first read.

The fix: ZREVRANGE for highest-first

docker exec redis-lesson redis-cli ZREVRANGE leaderboard 0 -1 WITHSCORES
bob
250
carol
175
alice
100

Same data, reverse order — ZREVRANGE is the one an actual leaderboard display wants. ZRANGE isn’t wrong or buggy; it’s documented to be ascending. The mistake is assuming “sorted set” implies “highest first” without checking.

Rank: two directions, same trap

docker exec redis-lesson redis-cli ZRANK leaderboard alice
docker exec redis-lesson redis-cli ZREVRANK leaderboard alice
0
2

ZRANK counts from the bottom — alice, the lowest score, is rank 0. ZREVRANK counts from the top — alice is in last place, rank 2 (of 3, zero-indexed). “What place is alice in” on a leaderboard means ZREVRANK, not ZRANK.

Atomic score updates

docker exec redis-lesson redis-cli ZINCRBY leaderboard 200 alice
docker exec redis-lesson redis-cli ZSCORE leaderboard alice
300
300

ZINCRBY adds to a score atomically and returns the new total — alice jumps from 100 to 300 in one round trip, safe even with concurrent scorers.

Range queries by score

docker exec redis-lesson redis-cli ZRANGEBYSCORE leaderboard 150 300
carol
bob
alice

“Everyone between 150 and 300 points” — carol (175), bob (250), and now alice (300, after the increment above) all qualify, returned in ascending score order same as plain ZRANGE.

Practice

1. Remove and return the highest-scoring member in one atomic operation.
docker exec redis-lesson redis-cli ZADD leaderboard 100 alice 250 bob 175 carol
docker exec redis-lesson redis-cli ZPOPMAX leaderboard
3
bob
250

ZPOPMAX reads and removes the top scorer atomically — useful for “process the current leader, then let someone else take the top spot” workflows without a separate read-then-delete race condition.

2. Query an exclusive range — strictly more than a given score.
docker exec redis-lesson redis-cli ZRANGEBYSCORE leaderboard "(100" 300
carol

Wrapping the lower bound in parentheses makes it exclusive — a plain 100 would include a member at exactly 100, "(100" excludes it. (bob was already popped in the previous task, so only carol at 175 remains in range.)

3. Get the total number of members without listing them.
docker exec redis-lesson redis-cli ZCARD leaderboard
2

ZCARD is O(1) — it reads a stored count rather than counting members on demand, so it stays fast regardless of how large the sorted set is.

Next: transactions — MULTI, EXEC, and the WATCH-based optimistic lock that can abort mid-flight.

Frequently Asked Questions

What makes a sorted set different from a regular set?
Every member has an associated floating-point score, and the set is always kept in score order internally — a regular set has no ordering at all. That score-ordering is what makes range queries ("everyone between 150 and 300 points") and rank queries ("what place is alice in") O(log N) operations instead of requiring a full scan and sort.
Why did ZRANGE list the lowest score first for a leaderboard?
ZRANGE always returns members in ascending score order — that's its documented, unsurprising behavior, but it's the opposite of what "leaderboard" usually implies (best score first). ZREVRANGE (or ZRANGE ... REV in modern Redis) returns descending order instead, and is almost always the one an actual leaderboard feature wants.
What's the difference between ZRANK and ZREVRANK?
ZRANK gives a member's position counting from the lowest score (rank 0 is the worst score); ZREVRANK counts from the highest score (rank 0 is the best). For a leaderboard's "what place is this player in," ZREVRANK is almost always the right one — ZRANK answers a different question.
Does ZINCRBY work the same way HINCRBY and INCR do?
Yes — it atomically adds to a member's score and returns the new score, the same atomicity guarantee as INCR for strings and HINCRBY for hash fields. Unlike those two, though, ZINCRBY doesn't reject non-numeric input by refusing — a sorted set's score is always a number by definition, so there's no WRONGTYPE-style error case here.
How do I query "everyone with more than 100 points" instead of a fixed range?
Wrap the lower bound in parentheses to make it exclusive: ZRANGEBYSCORE leaderboard "(100" +inf excludes anyone at exactly 100 and includes everyone above. +inf and -inf are valid bounds for "no upper/lower limit," which is how an open-ended range query is expressed.