Sets
Verified on Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24, Apple-silicon Mac. The# cli/...transcripts are one session in document order, replayed by the course harness. Timings are server-side microseconds fromSLOWLOG(threshold 0) on this machine: orders of magnitude, not benchmarks.
What is a Redis Set?
Section titled “What is a Redis Set?”A Redis Set is a collection of unique strings with no defined order. Adding a member that already exists is a no-op, so nothing is ever duplicated. That makes Sets the natural fit for tags, “already seen” checks and any group where each value appears once.
SADD and SREM — adding and removing members
Section titled “SADD and SREM — adding and removing members”SADD key member [member ...] adds one or more members and returns the count of new members (duplicates are not counted). SREM key member [member ...] removes members and returns how many were actually present.
127.0.0.1:6379> SADD article:1:tags "redis" "database" "caching"(integer) 3127.0.0.1:6379> SADD article:1:tags "redis"(integer) 0127.0.0.1:6379> SREM article:1:tags "caching"(integer) 1127.0.0.1:6379> SREM article:1:tags "nosql"(integer) 0127.0.0.1:6379> SCARD article:1:tags(integer) 2127.0.0.1:6379> OBJECT ENCODING article:1:tags"listpack"SCARD returns the number of members without fetching them. Note the last line: a small set of strings is stored as a listpack, not a hash table. The next section shows what that means for ordering.
SADD article:1:tags "redis" "database" "caching"
SADD article:1:tags "redis"
SREM article:1:tags "caching"
SREM article:1:tags "nosql"Three encodings, and the order you must not rely on
Section titled “Three encodings, and the order you must not rely on”A set picks one of three internal encodings by what it holds. The order SMEMBERS returns follows from the encoding, and only the fact that there is no guaranteed order is part of the contract:
127.0.0.1:6379> SADD s z a m(integer) 3127.0.0.1:6379> SMEMBERS s1) "z"2) "a"3) "m"127.0.0.1:6379> SADD n 30 10 20(integer) 3127.0.0.1:6379> SMEMBERS n1) "10"2) "20"3) "30"127.0.0.1:6379> OBJECT ENCODING n"intset"127.0.0.1:6379> SADD n x(integer) 1127.0.0.1:6379> OBJECT ENCODING n"listpack"127.0.0.1:6379> SMEMBERS n1) "10"2) "20"3) "30"4) "x"| Encoding | When (8.10.2 defaults) | What we saw |
|---|---|---|
intset | only integers, up to set-max-intset-entries (512) | 30 10 20 came back sorted: 10 20 30 |
listpack | small, mixed members, up to set-max-listpack-entries (128) | z a m came back in insertion order |
hashtable | anything bigger | a 600-member set is hashtable |
Both of those orders are implementation details of this version, not promises: the moment the set grows to a hashtable the order is unrelated to insertion. Anything that needs order belongs in a List or a Sorted Set. (An earlier version of this lesson showed invented orders for SMEMBERS; the transcripts here are real output.)
SMEMBERS and SCARD — inspecting a Set
Section titled “SMEMBERS and SCARD — inspecting a Set”SMEMBERS key returns every member; SCARD key returns the count. SMEMBERS is O(N) and is fine on a small set, not on a big one (measured below).
SADD user:42:interests "music" "hiking" "redis" "coffee"
SMEMBERS user:42:interests
SCARD user:42:interestsSISMEMBER and SMISMEMBER — membership check
Section titled “SISMEMBER and SMISMEMBER — membership check”SISMEMBER key member returns 1 or 0 in O(1), regardless of set size. SMISMEMBER key member [member ...] (6.2+) checks several members in one round trip and returns one flag per member, in order:
127.0.0.1:6379> SISMEMBER n 10(integer) 1127.0.0.1:6379> SMISMEMBER n 10 99 x1) (integer) 12) (integer) 03) (integer) 1SADD seen:urls "https://redis.io" "https://astro.build"
SISMEMBER seen:urls "https://redis.io"
SISMEMBER seen:urls "https://example.com"
SMISMEMBER seen:urls "https://redis.io" "https://example.com"SRANDMEMBER and SPOP — random access
Section titled “SRANDMEMBER and SPOP — random access”SRANDMEMBER key [count] returns random members without removing them; a negative count allows repeats and always returns exactly |count| items. SPOP key [count] removes and returns random members.
127.0.0.1:6379> SRANDMEMBER n 21) "10"2) "20"127.0.0.1:6379> SRANDMEMBER n -51) "30"2) "x"3) "30"4) "10"5) "x"127.0.0.1:6379> SPOP n 11) "x"127.0.0.1:6379> SCARD n(integer) 3The picks are random, so your output will differ. Five items came back from a four-member set because of the negative count (30 and x repeat).
SADD prize:pool "alpha" "beta" "gamma" "delta" "epsilon"
SRANDMEMBER prize:pool 2
SCARD prize:pool
SPOP prize:pool 1
SCARD prize:poolSINTERCARD — count an intersection without building it (7.0+)
Section titled “SINTERCARD — count an intersection without building it (7.0+)”SINTERCARD numkeys key [key ...] [LIMIT n] returns the size of the intersection, and stops early at LIMIT. The full set-algebra story is in Set operations.
127.0.0.1:6379> SADD a 1 2 3 4 5(integer) 5127.0.0.1:6379> SADD b 3 4 5 6(integer) 4127.0.0.1:6379> SINTERCARD 2 a b(integer) 3127.0.0.1:6379> SINTERCARD 2 a b LIMIT 2(integer) 2SSCAN — walk a big set without blocking
Section titled “SSCAN — walk a big set without blocking”I built a 200,000-member set (user:1 .. user:200000, hashtable, 8.8 MB) and asked SLOWLOG what each command cost the server:
| Command | Server time | Reply |
|---|---|---|
SISMEMBER big user:150000 | 5 µs | 1 integer |
SSCAN big 0 COUNT 100 | 44 µs | a cursor + about 100 members |
SMEMBERS big | 11,116 µs (11 ms) | 200,000 members (2.1 MB of member text) |
SMEMBERS freezes the command thread for 11 ms and floods the client; SSCAN returns a page at a time. On a small set SSCAN returns everything in one call:
127.0.0.1:6379> SADD big m1 m2 m3(integer) 3127.0.0.1:6379> SSCAN big 0 MATCH m* COUNT 201) "0"2) 1) "m1" 2) "m2" 3) "m3"A cursor of 0 in the reply means the scan is complete. Like SCAN, SSCAN can return the same member twice, and COUNT is a hint, not a page size.
From Node.js: tags, dedupe and a cursor walk
Section titled “From Node.js: tags, dedupe and a cursor walk”sadd returns the number of new members, so it doubles as “first time I have seen this?”. sscanStream wraps the cursor loop:
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);await redis.del('b2:post:1:tags', 'b2:seen');
// SADD returns how many members were NEW, so it doubles as "first time seen?"console.log(await redis.sadd('b2:post:1:tags', 'redis', 'cache', 'redis')); // 2 (the duplicate is ignored)console.log(await redis.sismember('b2:post:1:tags', 'redis')); // 1console.log(await redis.smismember('b2:post:1:tags', 'redis', 'sql')); // [ 1, 0 ]
// dedupe: process an event only the first time its id is addedasync function firstTime(id: string) { return (await redis.sadd('b2:seen', id)) === 1;}console.log(await firstTime('evt-1'), await firstTime('evt-1')); // true false
// iterate a big set with a cursor instead of SMEMBERSfor (let i = 0; i < 1000; i++) await redis.sadd('b2:seen', `evt-${i}`);let n = 0;for await (const batch of redis.sscanStream('b2:seen', { count: 200 })) n += (batch as string[]).length;console.log('scanned', n, 'members'); // 1000 (evt-1 was already there)
await redis.del('b2:post:1:tags', 'b2:seen');await redis.quit();Real output: 2, 1, [ 1, 0 ], true false, scanned 1000 members.
Reading AI-generated code
Section titled “Reading AI-generated code”An assistant writes the “does this user follow tag X?” check for a set that holds every tag the platform has ever seen:
🐛 Spot the bug
async function hasTag(tag: string) {const tags = await redis.smembers('tags:all'); // 200,000 membersreturn tags.includes(tag);}Show the answer
It works, and it downloads the whole set to answer a yes/no question. On a 200,000-member set SMEMBERS cost the server 11,116 µs and ships 2.1 MB of member text per call, against 5 µs and one integer for SISMEMBER. The array includes is also O(N) on the client. Ask the server the question you actually have:
async function hasTag(tag: string) { return (await redis.sismember('tags:all', tag)) === 1;}The runnable sets-sorted-sets-sets-tags.ts above uses sismember/smismember, and sscanStream for the case where you really do need every member.
Mental Model
Section titled “Mental Model”A Set is a bag with a membership test: “is X in it?” is constant time whatever the size, “give me everything” costs in proportion to the size, and “in what order?” has no answer. Design around the test (SISMEMBER, SADD’s return value), not around dumping the contents.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
| Set for tags / dedupe | Uniqueness with no existence check; SADD is its own upsert and tells you if it was new | No order (what you see is an encoding artifact); to sort or rank use a Sorted Set |
SISMEMBER / SMISMEMBER | O(1) per member, batched in one round trip | Only yes/no: metadata about the member lives in another key |
SMEMBERS | One call for everything | 11 ms of frozen server and 2.1 MB of text at 200k members; use SSCAN |
SPOP for random draws | Atomic sample-and-remove | The member is gone; log draws separately if you need history |
Common Misconceptions
Section titled “Common Misconceptions”- “
SMEMBERSreturns members in a stable order.” For a small set it is insertion order, for integers sorted, for a big set effectively arbitrary. None of that is guaranteed. - “
SADDreturns the set size.” It returns how many members were new. UseSCARDfor the size. - “
SRANDMEMBERwith a count returns that many distinct members.” Only a positive count is distinct; a negative count allows repeats and always returns|count|items. - “
SSCANreturns each member exactly once.” It may return a member more than once; it guarantees only that members present for the whole scan appear.
Common mistakes
Section titled “Common mistakes”- Calling
SMEMBERSon a big set in a request path — useSISMEMBERfor a question andSSCANfor a walk. - Checking membership one call at a time —
SMISMEMBER(or a pipeline) turns N round trips into one. - Using a Set when you need order — use a List, or a Sorted Set with a timestamp as the score.
Real-world example
Redis’s own documentation lists tagging and “unique items” as the standard uses of Sets and describes the three encodings and their limits in the Sets data-type page.