skip to content

Sets

You will learn unordered unique collections and their server-side algebra — intersections, unions, and differences computed where the data lives. Interviewers ask because tagging, mutual-friends, and deduplication questions all collapse into three set commands.

part ofRedisoverview, primer and where to startread it →
on this pageshow

questions

5

What is a Redis Set, and which commands do you use to add a member, remove one, test membership and get the element count?

level: juniorimportance: must knowfreq 72%

answer

  1. SADD returns count of NEW members
  2. SISMEMBER O(1), SCARD O(1)
  3. SMEMBERS is O(N) — full dump
  4. unordered, unique, binary-safe strings
  5. last SREM deletes the key

basics

~20 s

A Set is an unordered collection of unique strings. SADD adds (duplicates are silently ignored), SREM removes, SISMEMBER tests membership, SCARD returns the size, SMEMBERS returns everything. Add, remove, membership test and count are all O(1).

solid answer

~60 s

A Redis Set is an **unordered collection of unique string members**. Uniqueness is enforced by the server: `SADD key a b a` stores two members and returns 2 — the count of members actually *added*, not the count of arguments. The everyday commands: - `SADD key m...` — add one or more members, O(1) each. - `SREM key m...` — remove members, O(1) each. - `SISMEMBER key m` — 1/0 membership test, O(1). `SMISMEMBER` (7.0) tests several at once. - `SCARD key` — cardinality, O(1); it reads a stored counter, it does not walk the set. - `SMEMBERS key` — every member, O(N) — safe only for small sets. There is no ordering and no per-member metadata: if you need order or a score per element use a List or a Sorted Set, and if you need a value per member use a Hash. Removing the last member deletes the key, and a missing key behaves exactly like an empty set — `SISMEMBER` returns 0, `SCARD` returns 0, no error.

code

text · 13 lines
text
> SADD tag:kotlin item:1 item:2 item:1
(integer) 2                 # item:1 counted once
> SCARD tag:kotlin
(integer) 2                 # O(1), no scan
> SISMEMBER tag:kotlin item:2
(integer) 1
> SMISMEMBER tag:kotlin item:2 item:9
1) (integer) 1
2) (integer) 0
> SREM tag:kotlin item:1 item:2
(integer) 2
> EXISTS tag:kotlin
(integer) 0                 # empty set = no key

go deeper

for a junior

Name the five core commands, state that members are unique and unordered, and know that SADD's return value tells you whether the member was new.

for a middle

Add the cost model — O(1) for add/remove/test/count, O(N) for SMEMBERS — and explain when a Set is the wrong choice (need order, need a payload, need per-member TTL).

for a senior

Lead with the operational consequence: SMEMBERS on an unbounded set is a latency incident, SADD's return value is an atomic dedup primitive, and empty sets disappear as keys, which affects EXISTS-based logic.

for a principal

Frame Sets as an index primitive: cheap membership and set algebra bought with per-member memory overhead, no ordering, and key-level (not member-level) expiry — and say when that budget pushes you to a different structure or store.

## What a Set is A Redis Set is a collection of **unique strings with no defined order**. "Unique" is a server-side guarantee, not something your application has to check: adding a member that is already present is a no-op. "No order" is literal — the order in which members come back from `SMEMBERS` or `SSCAN` is an artifact of the internal storage layout and may change between calls, between versions, and between a primary and its replica. Never write code that depends on it. Members are binary-safe strings. That means `"1"` and `"01"` are different members, and `"User"` and `"user"` are different members — Redis compares bytes, never anything case- or locale-aware. ## The core command set **`SADD key member [member ...]`** adds members and returns *how many were new*. This return value is the single most useful thing about `SADD`: it is an atomic "was this the first time I saw this?" test. `SADD seen:2026-08-14 user:42` returning 1 means you just claimed the first sighting; returning 0 means someone (or an earlier retry of your own request) already did. Whole deduplication features are built on that one integer. **`SREM key member [member ...]`** removes members and returns how many were actually removed. Removing a member that is not there is not an error. When the last member is removed, Redis deletes the key entirely — an empty Set does not exist as a key in Redis, so `EXISTS` will start returning 0. **`SISMEMBER key member`** returns 1 or 0 in O(1). This is the workhorse for allow-lists, block-lists, feature flags per user, "has this user liked this post", and similar. `SMISMEMBER key m1 m2 m3` (Redis 7.0+) does the same for many members in one round trip and returns an array of 1/0 in argument order — prefer it over N separate calls, or pipeline the calls. **`SCARD key`** returns the number of members in O(1) because the cardinality is maintained as the set is mutated. Never compute a size as `len(SMEMBERS(key))` — that ships the whole set over the network to count it. **`SMEMBERS key`** returns every member. It is O(N) in the number of members and produces one reply containing all of them. That is fine for a set of a few dozen tags and dangerous for a set of millions of ids: the reply has to be built in memory and pushed through the client output buffer while the server does nothing else. For anything that can grow unbounded, iterate with `SSCAN` instead. **`SMOVE src dst member`** atomically moves a member between two sets — useful for state machines like `pending` → `processing` where a member must never be in both or neither. ## Missing keys and types A key that does not exist behaves as an empty set for every read: `SCARD` → 0, `SISMEMBER` → 0, `SMEMBERS` → empty array, `SREM` → 0. No `SCREATE` exists — `SADD` on a missing key creates it. If the key exists but holds a different type (say a String), any Set command returns a `WRONGTYPE` error; Redis will not coerce. Sets have no per-key element expiry: TTLs (`EXPIRE`, `TTL`) apply to the **whole key**, so you cannot expire a single member. Expiring individual members is emulated by using a Sorted Set with a timestamp score and pruning, or by bucketing members into per-time-window Set keys that expire as a unit. ## Why you would choose a Set Three jobs come up constantly: 1. **Membership** — "is X in the group?" answered in O(1) without transferring the group. 2. **Deduplication** — `SADD` returning 1 or 0 is an atomic first-time-seen test, which is why it appears in idempotency keys, unique-visitor counting and "only send this notification once". 3. **Tagging / relationships** — `tag:kotlin` → item ids, `user:42:following` → user ids, then combine sets server-side with `SINTER`/`SUNION`/`SDIFF`. ## Cost model to keep in your head `SADD`, `SREM`, `SISMEMBER`, `SCARD`, `SPOP`, `SMOVE` are O(1). `SMEMBERS` is O(N). `SSCAN` is O(1) per call, amortized O(N) over a full iteration. Memory per member is small but not free — it is dominated by the member string plus per-entry overhead, and Redis stores small all-integer sets far more compactly than string sets. Sizing a set that will hold tens of millions of members deserves an actual measurement, not a guess.

  • How do you use SADD as an idempotency or first-time-seen check?
    SADD returns the number of members that were actually new, so a return of 1 means this caller was the first to add that member and 0 means it was already present. Because the command is a single atomic operation on the server, two concurrent callers cannot both receive 1 for the same member. That makes `SADD processed:orders <orderId>` a race-free claim; pair the key with an EXPIRE so the dedup window does not grow forever.
  • What happens if you run SADD against a key that already holds a String?
    Redis returns a WRONGTYPE error and changes nothing — it never converts a key from one type to another implicitly. This is why key naming conventions matter: a collision between `user:42` as a String and `user:42` as a Set surfaces as runtime errors, not silent corruption. Namespacing keys by type or purpose (`user:42:tags`) avoids it.
  • How would you store a value alongside each member?
    You cannot — Set members carry no payload. Use a Hash when you need field→value, or a Sorted Set when the extra value is a number you want to order by. A common hybrid is a Set of ids for membership plus a Hash per id for its attributes, kept in sync by the writer.

Think of a guest list on the door: you only care whether a name is on it, adding the same name twice changes nothing, and nobody cares what order the names were written in.

saying these in an interview costs you the question

  • Believing SMEMBERS returns members in insertion order, or in any stable order at all
  • Computing set size client-side with len(SMEMBERS(key)) instead of SCARD
  • Checking membership in the application by fetching the whole set first
  • Thinking SADD errors or returns a failure when the member already exists
  • Expecting to set a TTL on an individual Set member rather than on the whole key

context

open as a page

A Redis Set holds several million members. What does SMEMBERS cost the server and the client, and how would you satisfy the common requests against that set — its size, whether specific values are in it, a random sample, the size of its overlap with another set — without ever materialising it?

level: middleimportance: must knowfreq 58%

basics

~20 s

SMEMBERS is O(N): one giant reply that occupies the single command thread and can blow the client output buffer, killing the connection. Ask set-native questions instead — SCARD for size, SISMEMBER/SMISMEMBER for membership, SRANDMEMBER for a sample, SINTERCARD with LIMIT for overlap size.

open as a page

Redis offers SINTER, SUNION and SDIFF plus SINTERSTORE, SUNIONSTORE and SDIFFSTORE. How do these behave, what do they cost, and when would you reach for the STORE variants?

level: middleimportance: should knowfreq 52%

basics

~20 s

They compute intersection, union and difference of Sets on the server. SINTER is roughly O(smallest set × number of sets), SUNION/SDIFF O(total members). STORE variants write the result into a destination key and return its size instead of shipping members — the destination is overwritten and gets no TTL.

open as a page

Redis has both SPOP and SRANDMEMBER for getting random elements out of a Set. How do they differ, and how does supplying a count — including a negative one — change what you get back?

level: middleimportance: should knowfreq 42%

basics

~20 s

SPOP removes and returns random members; SRANDMEMBER returns them without removing. With a positive count both return distinct members, capped at the set size. Only SRANDMEMBER accepts a negative count, which allows repeats and returns exactly that many elements.

open as a page

You are designing catalog filtering by multiple tags on top of Redis Sets — users pick several tags and see the matching items. How would you model it, and what breaks as the catalog and tag cardinalities grow?

level: principalimportance: should knowfreq 38%

basics

~20 s

Model one Set per tag holding item ids, plus a reverse Set per item for maintenance. AND is SINTER, OR is SUNION, exclusion SDIFF. It degrades when every selected tag is huge: the work happens in one blocking command. Mitigate with selective-tag ordering, SINTERCARD LIMIT, SINTERSTORE into short-TTL result keys, and precomputed hot combinations.

open as a page