ZINCRBY and Leaderboards
A sorted set in Redis keeps a group of unique members ordered by a floating-point score, and ZINCRBY is the command that atomically adds to (or subtracts from) a member’s score in one step. This single feature makes sorted sets the natural data structure for leaderboards, vote counts, and any ranking system where scores change constantly and concurrently. Because the increment happens as one atomic operation on Redis’s single thread, you never have to worry about two clients racing to update the same player’s score and clobbering each other’s work.
Overview: How ZINCRBY and Sorted Sets Work
Internally, a Redis sorted set (type zset) is backed by two structures kept in sync: a hash table mapping each member to its score for O(1) score lookups, and a skip list that keeps members ordered by score for fast range queries. A skip list is a linked structure with multiple “levels” of pointers, which lets Redis find, insert, or remove an element in O(log N) time on average without needing a balanced tree. Every command that touches a sorted set’s ordering — ZADD, ZINCRBY, ZRANGE, ZRANK — pays that O(log N) cost (or O(log N) + M for range results).
ZINCRBY key increment member reads the member’s current score from the hash table, adds increment to it, and reinserts the member at its new position in the skip list — all inside a single command execution. Because Redis is single-threaded, no other command can run in the middle of that read-modify-write; two clients calling ZINCRBY on the same player at nearly the same instant will both apply their full increment, in some serial order, with no lost updates. This is exactly what you want for a leaderboard: “add 10 points” from a thousand concurrent game clients should always add up to the correct total, never silently drop an update.
If the member does not already exist in the sorted set, ZINCRBY creates it and treats its starting score as 0, so the new score is simply the increment you passed. This means you never need to check whether a player already has a score before incrementing — the first ZINCRBY call for a new player both creates and scores them correctly.
Syntax
ZINCRBY key increment member
key— the name of the sorted set (the leaderboard).increment— a number (integer or floating-point) to add to the member’s score. Pass a negative number to subtract, e.g.-50.member— the element whose score is being changed (e.g. a player or user id). Created with a starting score of0plusincrementif it doesn’t already exist.
ZINCRBY replies with the member’s new score as a bulk string, so your application always knows the resulting total without a follow-up ZSCORE call.
| Command | Purpose | Time Complexity |
|---|---|---|
ZADD |
Set (or overwrite) a member’s score | O(log N) per added element |
ZINCRBY |
Atomically add to a member’s score | O(log N) |
ZSCORE |
Read a member’s current score | O(1) |
ZRANGE / ZREVRANGE |
List members in score order | O(log N + M), M = elements returned |
ZRANK / ZREVRANK |
Get a member’s position in the order | O(log N) |
Examples
Example 1: Incrementing a single player’s score
ZADD leaderboard:game1 100 "player:1"
ZINCRBY leaderboard:game1 50 "player:1"
ZSCORE leaderboard:game1 "player:1"
Output:
(integer) 1
"150"
"150"
ZADD creates the sorted set with player:1 starting at a score of 100, returning (integer) 1 because one new member was added. ZINCRBY then adds 50 to that score in one atomic step and returns the new total, "150", as a bulk string. The follow-up ZSCORE confirms the same value is now stored.
Example 2: Multiple players, negative increments, and ranking
ZADD leaderboard:global 500 "alice" 300 "bob"
ZINCRBY leaderboard:global 120 "alice"
ZINCRBY leaderboard:global -50 "bob"
ZINCRBY leaderboard:global 75 "carol"
ZRANGE leaderboard:global 0 -1 WITHSCORES
ZREVRANGE leaderboard:global 0 -1 WITHSCORES
Output:
(integer) 2
"620"
"250"
"75"
1) "carol"
2) "75"
3) "bob"
4) "250"
5) "alice"
6) "620"
1) "alice"
2) "620"
3) "bob"
4) "250"
5) "carol"
6) "75"
Alice and bob start the leaderboard. ZINCRBY raises alice’s score by 120 (to 620), lowers bob’s by 50 with a negative increment (to 250), and creates carol on the fly with a starting score of 75, since she didn’t exist before. ZRANGE ... WITHSCORES lists everyone from lowest to highest score (the default sorted-set order), while ZREVRANGE ... WITHSCORES lists them highest first — the order you actually want for a leaderboard display.
Example 3: A daily leaderboard with rank lookup and expiration
ZADD leaderboard:daily:2026-08-10 1200 "user:42" 950 "user:7" 1500 "user:19"
ZINCRBY leaderboard:daily:2026-08-10 300 "user:7"
ZREVRANK leaderboard:daily:2026-08-10 "user:7"
ZSCORE leaderboard:daily:2026-08-10 "user:7"
EXPIRE leaderboard:daily:2026-08-10 86400
TTL leaderboard:daily:2026-08-10
Output:
(integer) 3
"1250"
(integer) 1
"1250"
(integer) 1
(integer) 86400
Three players seed the daily leaderboard. After user:7 earns 300 more points, ZINCRBY reports their new total of 1250. ZREVRANK returns their 0-based rank counting from the top score — (integer) 1 means they’re in 2nd place, behind user:19‘s 1500. Finally, EXPIRE puts a 24-hour TTL on the whole leaderboard key so a “daily” leaderboard actually resets itself instead of accumulating forever, and TTL confirms the 86400 seconds is set.
How It Works Step by Step
When Redis receives ZINCRBY key increment member, it performs the following, entirely on the single command-processing thread before any other command is allowed to run:
- Look up
keyin the keyspace. If it doesn’t exist, an empty sorted set is created. - Look up
memberin the sorted set’s internal hash table (O(1)). If it’s not there, its current score is treated as 0. - Compute
new_score = current_score + increment. - Remove the member from its old position in the skip list (if it had one), then reinsert it at the position matching
new_score— this is the O(log N) step. - Update the hash table entry so future
ZSCORE/ZINCRBYlookups see the new score. - Return the new score to the client as a bulk string.
Because steps 2 through 5 all happen inside one command with no other client’s commands interleaved, there’s no window where a second ZINCRBY could read a stale score. Compare this to doing it yourself with ZSCORE to read, adding in your application code, then ZADD to write back — two round trips with a gap in between where another client’s update can be silently overwritten.
Common Mistakes
Mistake 1: Using ZADD to add points instead of ZINCRBY
ZADD leaderboard:game2 150 "player:1"
ZADD leaderboard:game2 10 "player:1"
ZSCORE leaderboard:game2 "player:1"
Output:
(integer) 1
(integer) 0
"10"
ZADD always sets a member’s score to the value you pass — it never adds to the existing score. The second call here doesn’t add 10 to player 1’s existing 150; it replaces it, and the returned (integer) 0 (no new member added, since it already existed) is an easy-to-miss clue that something silently overwrote instead of incrementing. The player’s earlier 150 points are gone. Use ZINCRBY whenever the intent is “add to the current score”:
ZADD leaderboard:game2 150 "player:1"
ZINCRBY leaderboard:game2 10 "player:1"
ZSCORE leaderboard:game2 "player:1"
Output:
(integer) 1
"160"
"160"
Mistake 2: Treating type errors as impossible
SET leaderboard:bad "not a sorted set"
ZINCRBY leaderboard:bad 10 "player:1"
Output:
OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value
Every Redis key has exactly one type. If leaderboard:bad was accidentally created as a plain string (say, by a typo’d SET somewhere else in your codebase), any sorted-set command against it fails with WRONGTYPE instead of silently doing something reasonable. Always use distinct, namespaced key names for different data types (e.g. keys under leaderboard: only ever hold sorted sets) and handle this error in application code rather than assuming a key always holds what you expect.
Mistake 3: Never expiring time-boxed leaderboards
A “weekly” or “daily” leaderboard that never gets a TTL just keeps accumulating scores forever, quietly wasting memory and showing stale rankings once the period is over. As shown in Example 3, call EXPIRE right after creating a periodic leaderboard key so Redis reclaims it automatically once the period ends, instead of relying on application code to remember to delete it.
Best Practices
- Use
ZINCRBY, not aZSCORE-then-ZADDpair, whenever you’re adding to an existing score — it’s both atomic and one round trip instead of two. - Namespace leaderboard keys clearly and consistently, e.g.
leaderboard:global,leaderboard:daily:2026-08-10, so different boards (and different data types) never collide. - Put a TTL on time-boxed leaderboards with
EXPIREso they clean themselves up; don’t rely on a cron job to delete them. - Use
ZREVRANGE/ZREVRANK(highest score first) for player-facing leaderboard displays, since sorted sets are ordered lowest-to-highest by default. - Prefer negative increments over separate “decrement” logic —
ZINCRBY key -10 membersubtracts cleanly using the same command. - For very large leaderboards, favor
ZRANGE/ZREVRANGEwith explicit start/stop indexes (e.g. top 10) over pulling the whole set — each returned element costs extra time, so scanning the full set is wasteful when you only display a page at a time.
Practice Exercises
- Create a sorted set
leaderboard:quiz1and give three players (e.g.user:a,user:b,user:c) starting scores of your choice withZADD. Then useZINCRBYto award points for two more “rounds” and useZREVRANGE ... WITHSCORESto confirm the final standings. - Pick one player from the exercise above and use
ZINCRBYwith a negative increment to dock 25 points. Confirm withZSCOREthat the score dropped by exactly 25, not that it got reset. - Build a small “today’s leaderboard” key, set a TTL of 60 seconds on it with
EXPIRE, and checkTTLimmediately after. Then work out (no need to actually wait) what a plainSETon that same key (instead ofZINCRBY) would do to the TTL you just set.
Summary
ZINCRBY key increment memberatomically addsincrementto a member’s score in a sorted set, creating the member with that score if it didn’t exist — O(log N) time.- Because Redis is single-threaded,
ZINCRBYhas no read-modify-write race window, unlike a manualZSCORE-then-ZADDpair. - Sorted sets are the right structure for leaderboards because members stay ordered by score automatically, unlike a plain set.
ZADDsets a score outright and will overwrite an existing one — use it to seed or reset, not to add points.- Calling any sorted-set command on a key holding a different type raises a
WRONGTYPEerror. - Use
ZREVRANGE/ZREVRANKfor highest-score-first display, and put a TTL on time-boxed leaderboards withEXPIRE.
