ZSCORE and ZRANK
Once you’ve built a sorted set with ZADD, the next questions you’ll ask are almost always the same two: “what score does this member have?” and “where does this member stand compared to everyone else?” ZSCORE answers the first question, and ZRANK (along with its mirror image, ZREVRANK) answers the second. Together they’re the workhorses behind leaderboards, priority queues, matchmaking systems, and any feature that needs to say “you are currently in Nth place.”
Overview / How it works
A Redis sorted set (zset) stores a unique set of string members, each associated with a floating-point score. Redis keeps members ordered by score at all times, so range and rank operations don’t need to sort anything on the fly — the ordering is maintained incrementally as you write. Internally, Redis backs a sorted set with two data structures working together once the set grows past a small size: a hash table that maps each member directly to its score, and a skip list that keeps members linked in score order. Small sorted sets (by default, fewer than 128 entries with short member values, controlled by the zset-max-listpack-entries and zset-max-listpack-value settings) instead use a compact, memory-efficient listpack encoding, which Redis scans linearly — fine because these sets are tiny.
This dual structure is exactly why ZSCORE and ZRANK have different costs. ZSCORE only needs the hash table: hash the member, look up its score, done — O(1), no matter how large the set is. ZRANK needs to know a member’s position among all other members, which requires walking the skip list’s ordered structure and accumulating a count as it goes — O(log N). Both commands are read-only and, like every Redis command, execute atomically on the single main thread: no other client can observe the sorted set mid-lookup or mid-update.
ZRANK returns a 0-based index in ascending score order (lowest score is rank 0). ZREVRANK returns the same idea but in descending order (highest score is rank 0) — the natural choice for “1st place” leaderboards where a bigger score is better. If two members have the same score, Redis breaks the tie by comparing the member strings lexicographically, so ranks are always well-defined and stable.
Syntax
ZSCORE key member
ZMSCORE key member [member ...]
ZRANK key member [WITHSCORE]
ZREVRANK key member [WITHSCORE]
| Argument | Meaning |
|---|---|
key |
The name of the sorted set. |
member |
The member whose score or rank you want. Must match exactly, including case. |
WITHSCORE |
Optional (Redis 7.2+). Returns the member’s score alongside its rank in one round trip, avoiding a separate ZSCORE call. |
| Command | Time Complexity | Returns |
|---|---|---|
ZSCORE |
O(1) | The score as a bulk string, or (nil) if the member or the key doesn’t exist. |
ZMSCORE |
O(N) for N members requested | An array of scores in request order, with (nil) for any member not found — one round trip instead of many. |
ZRANK |
O(log(N)) | 0-based rank ascending by score, or (nil) if the member/key doesn’t exist. |
ZREVRANK |
O(log(N)) | 0-based rank descending by score, or (nil) if the member/key doesn’t exist. |
Examples
Example 1: Basic score and rank lookup
ZADD leaderboard:global 1500 "alice"
ZADD leaderboard:global 2200 "bob"
ZADD leaderboard:global 1800 "carol"
ZSCORE leaderboard:global "bob"
ZRANK leaderboard:global "bob"
ZRANK leaderboard:global "alice"
Output:
(integer) 1
(integer) 1
(integer) 1
"2200"
(integer) 2
(integer) 0
Three members go in with scores 1500, 1800, and 2200. ZSCORE reports bob’s raw score, "2200". Ranked ascending by score, alice (1500) is lowest at rank 0, carol (1800) is rank 1, and bob (2200) is highest at rank 2 — exactly what ZRANK returns for each.
Example 2: ZREVRANK and WITHSCORE for a “place” leaderboard
ZADD contest:scores 340 "team-red"
ZADD contest:scores 512 "team-blue"
ZADD contest:scores 210 "team-green"
ZREVRANK contest:scores "team-blue"
ZRANK contest:scores "team-blue"
Output:
(integer) 1
(integer) 1
(integer) 1
(integer) 0
(integer) 2
Here higher is better, so ZREVRANK is the right tool: team-blue has the top score (512), so its descending rank is 0 — first place. The plain ascending ZRANK for the same member is 2 (it’s the highest of three, so last in ascending order). Redis 7.2+ also supports an optional WITHSCORE argument on ZRANK/ZREVRANK that returns the score alongside the rank in one call, saving a separate ZSCORE round trip together in a single call instead of two round trips.
Example 3: Updating a score and handling missing members
ZADD game:scores 100 "player1"
ZADD game:scores 250 "player2"
ZINCRBY game:scores 200 "player1"
ZSCORE game:scores "player1"
ZRANK game:scores "player1"
ZSCORE game:scores "player3"
ZRANK game:scores "player3"
Output:
(integer) 1
(integer) 1
"300"
"300"
(integer) 1
(nil)
(nil)
ZINCRBY bumps player1’s score by 200, from 100 to 300, and returns the new total directly. After the increment, player1 (300) now outranks player2 (250), so player1’s ZRANK becomes 1. player3 was never added, so both ZSCORE and ZRANK correctly return (nil) rather than an error — always check for (nil) before treating a result as a real score or rank.
Example 4: Batch score lookups with ZMSCORE
ZADD inventory:prices 19.99 "widget"
ZADD inventory:prices 5.49 "gadget"
ZMSCORE inventory:prices "widget" "gadget" "gizmo"
Output:
(integer) 1
(integer) 1
1) "19.989999999999998"
2) "5.4900000000000002"
3) (nil)
ZMSCORE fetches several scores in one round trip instead of calling ZSCORE repeatedly. Requested members come back in the same order you asked for them, and “gizmo” — which was never added — comes back as (nil) in its slot rather than breaking the whole reply.
How it works step by step
When you run ZSCORE key member, Redis looks up key in the main keyspace dictionary, confirms it’s a zset, then does a single hash lookup of member against the zset’s internal member-to-score hash table. That’s the entire operation — no traversal of any ordering structure is needed, which is why it’s O(1).
When you run ZRANK key member, Redis first does that same hash lookup to confirm the member exists (and to know its score). If the set is skip-list encoded, Redis then walks the skip list’s forward pointers starting from the highest level, summing each pointer’s “span” (the number of elements it skips over) until it reaches the target member’s score/member position. This span-summing trick lets the skip list report a position in O(log N) hops instead of counting one element at a time. For small, listpack-encoded sets, Redis just scans the compact list directly — technically O(N), but N is small enough by design that it’s effectively instant.
Common Mistakes
Mistake 1: Calling ZSCORE or ZRANK on a key that isn’t a sorted set. Every Redis key has exactly one type, and sorted-set commands refuse to operate on a key holding a string, hash, list, or anything else.
SET user:1001:name "Ada"
ZSCORE user:1001:name "Ada"
Output:
OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value
The fix is simply to use a distinct key for the sorted set — for example ZSCORE user:1001:scores "Ada" against a key that was actually built with ZADD, never a key that already holds a string.
Mistake 2: Assuming ranks are 1-indexed. It’s easy to display a rank straight from Redis and get an off-by-one “place” for users.
ZADD race:results 9.58 "bolt"
ZADD race:results 9.69 "powell"
ZRANK race:results "bolt"
Output:
(integer) 1
(integer) 1
(integer) 0
Bolt has the fastest (lowest) time and is genuinely in first place, but ZRANK reports that as 0, not 1, because ranks are always zero-based. Whenever you show a rank to a human, add 1 first — and remember to use ZREVRANK instead of ZRANK whenever a higher score, not a lower one, is what makes someone rank first.
Mistake 3: Looping ZRANK or ZSCORE over every member instead of using a bulk command. Calling ZSCORE once per member in application code, in a loop, turns one network round trip into hundreds. Use ZMSCORE for several specific members, or ZRANGE key 0 -1 WITHSCORES when you actually need the whole set, instead of hammering the server with individual lookups.
Best Practices
- Use
ZREVRANKfor “higher score wins” leaderboards and plainZRANKfor “lower is better” rankings like race times or golf scores. - Always add 1 to a returned rank before displaying it as a human-facing position — Redis ranks start at 0.
- Check for
(nil)explicitly on bothZSCOREandZRANK; a missing member is not an error, but treating a(nil)as a score of 0 will silently corrupt your logic. - Reach for
ZMSCOREor a range command instead of looping single-member calls when you need data for many members at once. - Use the
WITHSCOREoption onZRANK/ZREVRANK(Redis 7.2+) when you need both the rank and the score, to save a round trip. - Remember scores are returned as strings — parse them back into a number in your application before doing arithmetic on them.
Practice Exercises
- Create a sorted set
exam:scoreswith three students and distinct scores. UseZRANKto find the lowest scorer andZREVRANKto find the top scorer, and confirm the two ranks point to different students unless there’s a tie. - Build a weekly game leaderboard
game:weeklywith at least four players. UseZREVRANKto compute the human-facing place (remember the +1) for one specific player, then useZINCRBYto change their score and re-check whether their place changed. - Add a handful of members to a set called
catalog:ratings, then callZMSCOREwith a mix of real member names and one made-up name. Confirm the made-up name comes back as(nil)in the correct position of the result array.
Summary
ZSCOREretrieves a single member’s score in O(1) via a direct hash lookup — it never touches the ordering structure.ZRANKandZREVRANKretrieve a member’s 0-based position in O(log(N)) by walking the sorted set’s skip list.ZRANKorders ascending (lowest score = rank 0);ZREVRANKorders descending (highest score = rank 0).- Both commands return
(nil), not an error, for a member or key that doesn’t exist — always check for it. - Use
ZMSCOREto fetch several scores in one round trip instead of loopingZSCOREcalls. - Calling any sorted-set command on a key holding a different type raises a
WRONGTYPEerror — sorted sets need their own dedicated keys.
