Sets Explained

A Redis Set is an unordered collection of unique strings stored under a single key. Unlike a List, a Set has no order and no duplicates — adding the same member twice is a no-op. Sets are the natural fit whenever you need fast membership tests, deduplication, or set algebra (union, intersection, difference) — think tags on a blog post, unique visitors for a day, or mutual followers between two users.

Overview / How Sets Work

Internally, a Redis Set is backed by one of three encodings, and Redis automatically picks (and upgrades) the encoding based on the data you store, without you having to think about it:

  • intset — used when every member is an integer and the set is small (up to the set-max-intset-entries limit, default 512). This is a sorted array of integers and is extremely memory-efficient.
  • listpack — used for small sets of short strings (up to set-max-listpack-entries members, default 128, each no longer than set-max-listpack-value bytes, default 64). A listpack is a compact, contiguous memory blob — fast to scan for small sizes, but a linear scan nonetheless.
  • hashtable — once a set grows past either of the thresholds above, Redis converts it (permanently — it never converts back down) to a real hash table. This is what gives Sets their signature O(1) average-case SADD, SREM, and SISMEMBER at scale.

This matters practically: a set of a few dozen short tag strings and a set of a million user IDs behave very differently under the hood, even though you call the exact same commands on both. The conversion is one-directional and automatic — you never manage it yourself.

Because Redis is single-threaded, every individual Set command — even SADD with ten members, or SINTERSTORE across five sets — runs to completion without any other command interleaving. That’s what makes Sets safe for concurrent access from many clients: two clients calling SADD tags:post:42 redis at the same time can never race each other or corrupt the set; Redis simply processes one call, then the other.

Sets shine for: unique visitor/impression counters (add a user ID per event, SCARD gives you the unique count for free), tagging systems, access-control lists, deduplication of a stream of IDs, and set algebra for finding relationships — mutual friends, shared interests, or common inventory between two collections. What Sets do not give you is order: if you need members ranked by a score (a leaderboard, a priority queue), reach for a Sorted Set (ZADD/ZRANGE) instead — a plain Set cannot tell you which member is “biggest.”

Syntax

The general shapes of the commands covered in this lesson:

SADD key member [member ...]
SREM key member [member ...]
SISMEMBER key member
SMISMEMBER key member [member ...]
SMEMBERS key
SCARD key
SPOP key [count]
SRANDMEMBER key [count]
SINTER key [key ...]
SUNION key [key ...]
SDIFF key [key ...]
SINTERSTORE destination key [key ...]
SUNIONSTORE destination key [key ...]
SDIFFSTORE destination key [key ...]
SSCAN key cursor [MATCH pattern] [COUNT count]
Argument Meaning
key The Set key name. Auto-created on the first SADD; auto-deleted when the last member is removed.
member A string value to add, remove, or test. Sets silently ignore duplicate adds.
count Optional, for SPOP/SRANDMEMBER: how many random members to return.
destination Key that receives the result of a store variant (overwritten if it already exists).
cursor Opaque iteration position for SSCAN; start with 0, keep calling with the returned cursor until it comes back as 0 again.
Command Time Complexity
SADD O(N) for N members added (O(1) each)
SREM O(N) for N members removed
SISMEMBER O(1)
SMISMEMBER O(N) for N members checked
SCARD O(1)
SMEMBERS O(N) — returns the whole set
SPOP / SRANDMEMBER O(1) without count, O(N) with it
SINTER / SINTERSTORE O(N*M), N = smallest input set’s size, M = number of sets
SUNION / SDIFF (and *STORE) O(N) = total elements across all input sets
SSCAN O(1) per call, O(N) to walk the full set

Examples

Example 1: Basic membership

SADD fruits:basket apple banana cherry
SADD fruits:basket apple
SMEMBERS fruits:basket
SISMEMBER fruits:basket banana
SISMEMBER fruits:basket mango
SCARD fruits:basket

Output:

(integer) 3
(integer) 0
1) "apple"
2) "banana"
3) "cherry"
(integer) 1
(integer) 0
(integer) 3

The first SADD creates the key and adds three new members, so it returns 3. The second SADD tries to add apple again — already present, so 0 new members were added and the set is unchanged. SMEMBERS returns all members; note that a Set has no guaranteed order, so don’t rely on the sequence you see (small listpack-encoded sets often print in insertion order in practice, but this is an implementation detail, not a contract). SISMEMBER is an O(1) existence check, distinct from scanning the whole set.

Example 2: Set algebra for shared interests

SADD user:1001:interests redis python docker
SADD user:1002:interests python java docker
SINTER user:1001:interests user:1002:interests
SUNION user:1001:interests user:1002:interests
SDIFF user:1001:interests user:1002:interests
SINTERSTORE user:1001:1002:common user:1001:interests user:1002:interests
SMEMBERS user:1001:1002:common

Output:

(integer) 3
(integer) 3
1) "python"
2) "docker"
1) "redis"
2) "python"
3) "docker"
4) "java"
1) "redis"
(integer) 2
1) "python"
2) "docker"

This is the classic “people you may know” or “shared tags” pattern. SINTER returns members present in both sets (here, python and docker) without modifying either input. SUNION returns every distinct member across both sets. SDIFF returns members in the first set that are not in the second (order of arguments matters for SDIFF, unlike SINTER/SUNION). SINTERSTORE does the same computation as SINTER but writes the result into destination and returns the resulting cardinality instead of the members themselves — handy for caching an expensive intersection.

Example 3: Random selection and removal

SADD raffle:tickets alice bob carol dave
SCARD raffle:tickets
SRANDMEMBER raffle:tickets
SMISMEMBER raffle:tickets alice zoe
SREM raffle:tickets bob
SPOP raffle:tickets
SMEMBERS raffle:tickets

Output:

(integer) 4
(integer) 4
"carol"
1) (integer) 1
2) (integer) 0
(integer) 1
"carol"
1) "alice"
2) "dave"

SRANDMEMBER returns a random member without removing it — call it again and you might see the same or a different member each time (the exact name shown here is illustrative; yours will vary). SMISMEMBER checks several members at once, returning 1/0 for each in order — useful to avoid N round trips of SISMEMBER. SREM deletes specific members and returns how many were actually removed (removing a member that isn’t there costs nothing and returns 0). SPOP is the destructive sibling of SRANDMEMBER: it removes and returns a random member in one atomic step, which is exactly why it’s the right tool for a fair raffle draw or a work queue that hands out one random job at a time.

Example 4: Iterating large sets safely

SADD colors:palette red green blue yellow orange
SSCAN colors:palette 0

Output:

1) "0"
2) 1) "red"
   2) "green"
   3) "blue"
   4) "yellow"
   5) "orange"

SSCAN returns a cursor plus a batch of members. A returned cursor of "0" means the iteration is complete (as it is here, since the set is tiny); for a large set you’d keep calling SSCAN key <last-cursor>, feeding each returned cursor back in, until you get 0 again. Unlike SMEMBERS, which grabs the entire set in one blocking pass, SSCAN walks it incrementally, so the single-threaded server stays responsive to other clients between calls — the same reasoning that makes SCAN preferable to KEYS across the whole keyspace applies to SSCAN versus SMEMBERS on one very large set.

How It Works Step by Step

When you run SADD key member: (1) Redis looks up key in the main dictionary; if it doesn’t exist, a new Set object is created, starting as an intset or listpack depending on the member’s type. (2) The member is hashed and checked against the existing contents for uniqueness — this is what makes duplicate adds a no-op. (3) If the member is new, it’s inserted, and the return counter increments. (4) After insertion, Redis checks whether the set has crossed a size or value-length threshold; if so, it’s transparently converted to the next larger encoding (listpack → hashtable). (5) The whole operation completes before any other client’s command runs, because Redis executes commands one at a time on its single main thread — there’s no window where a concurrent SADD or SREM could interleave and produce a lost update.

For SINTER, Redis doesn’t blindly compare every pair of elements: it picks the smallest input set and iterates only its members, checking each one against the other sets’ hash tables for O(1) membership tests — which is why the complexity is expressed in terms of the smallest set’s size, not the largest.

Common Mistakes

Mistake 1: Using a String key holding delimited values instead of a real Set. Storing "redis,python,docker" as a String and manually splitting it on commas throws away O(1) membership tests, forces you to rewrite the whole value on every add/remove, and breaks the moment a tag itself contains a comma. Use a Set so SADD/SREM/SISMEMBER do the right thing natively.

Mistake 2: Calling a Set command on a key that already holds a different type.

SET session:abc123 "logged-in"
SADD session:abc123 admin

Output:

OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value

Every key has exactly one type. Because session:abc123 already holds a String, SADD on it fails outright rather than silently converting the key. The fix is to use a distinct, purpose-specific key name (e.g. session:abc123:roles) rather than overloading one key for two different data shapes.

Mistake 3: Assuming EXPIRE on a key that doesn’t exist does something.

EXPIRE tags:nonexistent 60

Output:

(integer) 0

EXPIRE returns 0 (and does nothing) when the key isn’t there — it does not create the key or error. Always check the return value if your logic depends on the TTL actually being set, and remember that SCARD/TTL on a nonexistent Set key will report 0 and -2 respectively rather than erroring.

Mistake 4: Reaching for SMEMBERS on a set with millions of members. SMEMBERS is O(N) and returns the entire set in a single reply, blocking the single-threaded server for the duration and potentially sending a huge payload over the wire. For large sets, page through with SSCAN instead (see Example 4).

Best Practices

  • Use Sets for membership and deduplication, not for anything that needs order or a numeric score — that’s a Sorted Set’s job.
  • Prefer SSCAN over SMEMBERS once a set could realistically grow past a few thousand members.
  • Batch multiple members into one SADD/SREM call instead of looping one command per member — fewer round trips, same atomicity guarantee.
  • Use SMISMEMBER instead of N separate SISMEMBER calls when checking several members at once.
  • Use the *STORE variants (SINTERSTORE, SUNIONSTORE, SDIFFSTORE) to cache an expensive set computation as its own key rather than recomputing it on every read.
  • Set a TTL with EXPIRE on Sets that represent time-bound data (daily unique visitors, per-session tag lists) so they don’t accumulate forever and leak memory.
  • Give Set keys a clear, colon-namespaced name that reflects both the entity and the relationship (user:1001:followers, not followers1001).
  • Remember SPOP/SRANDMEMBER selection is not cryptographically secure random — fine for raffles and sampling, not for security tokens.

Practice Exercises

Exercise 1: Create two Sets, user:2001:following and user:2002:following, each with a few usernames. Find the accounts both users follow in common, then store that intersection under a new key so it can be read back later without recomputing it.

Exercise 2: Simulate a daily unique-visitor counter: add several user IDs to a key like visitors:2026-08-10, then use a single O(1) command to report how many unique visitors there were — without printing the full member list. Set an appropriate TTL so old days’ data doesn’t accumulate forever.

Exercise 3: Build a simple tag cloud: add tags to post:501:tags and post:502:tags, then compute the full set of distinct tags used across both posts in one command, and separately find any tag on post 501 that isn’t used on post 502.

Summary

  • A Set is an unordered collection of unique strings; duplicates are silently ignored on SADD.
  • Internally encoded as intset, listpack, or hashtable depending on size and content — Redis upgrades the encoding automatically and never downgrades it.
  • SISMEMBER, SCARD, and SADD/SREM per member are O(1); SMEMBERS, SUNION, and SDIFF are O(N) over the total elements involved.
  • SINTER/SUNION/SDIFF (and their *STORE variants) implement set algebra directly in Redis — no need to pull data into your application to compute it.
  • SPOP removes a random member atomically; SRANDMEMBER peeks without removing.
  • Every command is atomic thanks to Redis’s single-threaded execution model — no manual locking needed for concurrent Set updates.
  • Prefer SSCAN over SMEMBERS for large sets, and remember a Set has no ordering — use a Sorted Set when order or scoring matters.