Redis Cheatsheet
Sets
Use this Redis reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Command table
An unordered collection of unique strings. Membership is O(1), and the set-algebra commands run server side, which is the whole reason to use one: intersecting two follower sets never leaves Redis.
| Command | What it does | Complexity |
|---|---|---|
SADD key m [m ...] | Add members, return how many were new | O(n) members |
SREM key m [m ...] | Remove members, return how many existed | O(n) members |
SISMEMBER key m | 1 or 0 | O(1) |
SMISMEMBER key m [m ...] | One 1/0 per member, in order (6.2+) | O(n) members |
SCARD key | Cardinality, 0 if absent | O(1) |
SMEMBERS key | Every member | O(n) set size |
SPOP key [count] | Remove and return random members | O(count) |
SRANDMEMBER key [count] | Return random members without removing | O(count) |
SMOVE src dst m | Atomically move one member between sets | O(1) |
SSCAN key cursor [MATCH p] [COUNT n] | Cursor iterate | O(1) per call |
SINTER k [k ...] | Intersection | O(n×m) worst case |
SUNION k [k ...] | Union | O(total members) |
SDIFF k [k ...] | First set minus the rest | O(total members) |
SINTERSTORE dst k [k ...] | Intersection into a key | O(n×m) |
SUNIONSTORE dst k [k ...] | Union into a key | O(total) |
SDIFFSTORE dst k [k ...] | Difference into a key | O(total) |
SINTERCARD n k [k ...] [LIMIT l] | Size of the intersection, no result set (7.0+) | O(n×m) |
SMEMBERS key is exactly SINTER key, and both are O(n) with the entire set on the wire.
Use SSCAN above a few thousand members.
Basics
SADD hu:post:42:tags redis cache database # (integer) 3, and a re-add of "redis" returns 0 SISMEMBER hu:post:42:tags redis # 1 SMISMEMBER hu:post:42:tags redis mysql # 1) 1 2) 0 SCARD hu:post:42:tags # 3 SREM hu:post:42:tags cache # 1 SMEMBERS hu:post:42:tags # redis database, order is arbitrary
Order is not just unspecified, it genuinely varies with the encoding and with insertion history. Never rely on it, and never rely on it being stable between two calls.
Removing the last member deletes the key, so an empty set does not exist and EXISTS
returns 0. SCARD on a missing key is 0.
Random members, and the sign rule
SPOP removes, SRANDMEMBER does not. Both take an optional count whose sign changes
the semantics.
| Call | Behavior |
|---|---|
SPOP key | One member, removed |
SPOP key 3 | Up to 3 distinct members, removed |
SRANDMEMBER key 3 | Up to 3 distinct members, capped at the set size |
SRANDMEMBER key -3 | Exactly 3 draws with repetition |
SADD hu:raffle:9 a b c > /dev/null SRANDMEMBER hu:raffle:9 10 # a b c, capped, never repeats SRANDMEMBER hu:raffle:9 -10 # 10 entries, repeats allowed SPOP hu:raffle:9 2 # two winners, gone from the set
SPOP with a count is not replicated as SPOP to replicas, it is rewritten as an
explicit SREM, which is what keeps a replica consistent with a nondeterministic command.
That same nondeterminism is why SPOP inside a Lua script was historically restricted.
Set algebra
The three operations plus their STORE forms are the reason to model relationships as
sets.
SADD hu:user:1:follows 10 11 12 > /dev/null SADD hu:user:2:follows 11 12 13 > /dev/null SINTER hu:user:1:follows hu:user:2:follows # 11 12, mutual SUNION hu:user:1:follows hu:user:2:follows # 10 11 12 13 SDIFF hu:user:1:follows hu:user:2:follows # 10, only user 1 follows SDIFF hu:user:2:follows hu:user:1:follows # 13, order matters # Persist the answer so you can paginate it with SSCAN SINTERSTORE hu:tmp:mutual:1:2 hu:user:1:follows hu:user:2:follows EXPIRE hu:tmp:mutual:1:2 60
SDIFF is not commutative, it is the first key minus every other key. The STORE
variants overwrite dst completely, and an empty result deletes dst rather than
leaving an empty set, which quietly breaks code that assumes the destination exists.
SINTERCARD (7.0+) answers "how many" without materializing the intersection, and
LIMIT lets it stop early.
SINTERCARD 2 hu:user:1:follows hu:user:2:follows # 2 SINTERCARD 2 hu:user:1:follows hu:user:2:follows LIMIT 1 # 1, stopped at 1
LIMIT 1 turns "do these sets overlap at all?" into a cheap question on huge sets, since
it aborts on the first hit instead of counting every match. LIMIT 0 means no limit.
Redis optimizes SINTER by starting from the smallest set and probing the others, so
intersecting a 10-element set against a 10-million-element set is fast. SUNION and
SDIFF have no such shortcut and are O(total members) across every key.
Tag and relationship patterns
Tags, both directions. Keep a set per post and a set per tag, and every interesting query becomes set algebra.
SADD hu:post:42:tags redis cache SADD hu:tag:redis:posts 42 SADD hu:tag:cache:posts 42 43 SINTER hu:tag:redis:posts hu:tag:cache:posts # posts tagged BOTH SUNION hu:tag:redis:posts hu:tag:cache:posts # posts tagged EITHER SDIFF hu:tag:cache:posts hu:tag:redis:posts # cache but not redis
Both directions must be written together or they drift, so wrap the two SADD calls in
MULTI or a Lua script.
Followers and suggestions.
# People user 1 follows who also follow user 1 back SINTER hu:user:1:follows hu:user:1:followers # Suggestions: friends of a friend that user 1 does not already follow SDIFFSTORE hu:tmp:sugg:1 hu:user:2:follows hu:user:1:follows SREM hu:tmp:sugg:1 1 # never suggest yourself
Deduplication. SADD returning 1 means "first time I have seen this", which is a
one-command idempotency guard.
if [ "$(redis-cli SADD hu:seen:webhook "$event_id")" = "1" ]; then process "$event_id" fi redis-cli EXPIRE hu:seen:webhook 86400
That is exact but grows with the number of ids. When an approximate count is enough, switch to a HyperLogLog, which is a fixed 12KB at 0.81 percent error.
Unique daily visitors, exactly.
SADD hu:visitors:2026-09-15 "user:1042" SCARD hu:visitors:2026-09-15 SINTERSTORE hu:retained hu:visitors:2026-09-15 hu:visitors:2026-09-14
Encoding and sizing
SADD s 1 2 3 ; OBJECT ENCODING s # "intset", every member parses as an integer SADD s2 a b ; OBJECT ENCODING s2 # "listpack", small and non-integer (7.2+)
Three encodings, and conversion is one way:
| Encoding | Used when | Cost |
|---|---|---|
intset | All members are integers, count ≤ set-max-intset-entries (512) | A sorted int array, tiny |
listpack | Small non-integer sets, ≤ set-max-listpack-entries (128) and -value (64) | Flat, tiny, O(n) lookup |
hashtable | Anything larger | O(1) lookup, ~50+ bytes per member |
An all-integer set is by far the cheapest structure in Redis, so store numeric ids as
integers rather than "user:1042" when you have millions of them. Adding one non-integer
member converts the whole set permanently.
Set or something else
| Need | Use |
|---|---|
| Unique membership, O(1) checks | Set |
| Unique and ordered by a score | Sorted set |
| Duplicates allowed, FIFO | List |
| Approximate unique count at scale | HyperLogLog, PFADD and PFCOUNT |
| Field-value pairs on one entity | Hash |
| Top-N by count | Sorted set with ZINCRBY |
The line to remember: a set answers "is X in here" and "what do these two have in common". The moment the question involves ranking, ordering, or a range, it is a sorted set.