SADD, SMEMBERS, and SREM

A Redis set is an unordered collection of unique strings attached to a key. SADD adds one or more members to a set, SMEMBERS returns every member currently in the set, and SREM removes one or more members from it. Together these three commands are the foundation for anything that needs fast, automatic de-duplication: unique visitor tracking, tag lists, permission sets, and “has this user already done X” checks.

Because a set enforces uniqueness for you at the data-structure level, you never have to write application code that checks “does this value already exist” before inserting it — Redis does that check internally, atomically, on every SADD call.

Overview: How Redis Sets Work

Internally, a Redis set is backed by one of two encodings, and Redis switches between them automatically based on size and content, entirely transparent to you as a user:

  • intset — used when every member is an integer and the set is small. This is extremely memory-efficient because it stores members as a sorted array of integers rather than full string objects.
  • listpack / hashtable — used for small non-integer sets (listpack, a compact packed encoding) and for larger or mixed-content sets (a real hash table, giving O(1) average-case membership tests). Once a set grows past the configured entry-count or value-size thresholds, Redis converts it to the hashtable encoding permanently.

Regardless of encoding, the guarantee you get as a user is the same: every member is unique, membership tests are fast, and there is no ordering — a set is not a list and not a sorted set. If you need members ordered by score, that’s a job for a sorted set (ZADD/ZRANGE), not a plain set.

Redis is single-threaded: it executes one command to completion before starting the next. This means SADD and SREM are always atomic — if two clients call SADD leaderboard:entrants alice at nearly the same instant, there is no race condition and no possibility of a lost update, because the server simply processes one call fully, then the other. This is exactly why sets are a good fit for things like “track unique visitors today” from multiple concurrent request handlers without any external locking.

Like any Redis key, a set key can carry a TTL via EXPIRE, independent of the SADD/SREM operations that change its contents. If a set’s last member is removed with SREM, Redis deletes the key entirely — an empty set is not a valid stored value, so EXISTS on that key will then return 0.

Syntax

SADD key member [member ...]
SMEMBERS key
SREM key member [member ...]
  • key — the name of the set (use a colon-namespaced convention, e.g. tags:post:100).
  • member (SADD) — one or more string values to add. Duplicates within the same call, or members already present, are simply not re-added.
  • member (SREM) — one or more members to remove. Members that aren’t present are silently ignored, not treated as errors.
Command Time Complexity Notes
SADD O(N) for N members added (O(1) per member) Returns the count of members actually added (excludes duplicates)
SMEMBERS O(N) where N is the set’s cardinality Returns ALL members at once — risky on very large sets
SREM O(N) for N members to be removed Returns the count of members actually removed
SCARD O(1) Returns the number of members without transferring them
SISMEMBER O(1) Tests membership of a single value

Examples

Example 1: Adding and listing members

SADD fruits:basket apple banana cherry
SMEMBERS fruits:basket
SADD fruits:basket apple
SMEMBERS fruits:basket

Output:

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

The first SADD adds three new members and returns 3 — the count of members that were actually added. The second SADD tries to add apple again; since it’s already a member, nothing changes and the return value is 0. Note that SMEMBERS does not guarantee any particular ordering — it happens to print in insertion order here for a small hashtable-encoded set, but you should never depend on that order in application logic.

Example 2: Removing members

SADD tags:post:100 redis database nosql cache
SREM tags:post:100 nosql
SMEMBERS tags:post:100
SREM tags:post:100 graphql

Output:

(integer) 4
(integer) 1
1) "redis"
2) "database"
3) "cache"
(integer) 0

Four tags are added in one call. SREM tags:post:100 nosql removes exactly that one member and returns 1 (the count removed). Trying to remove graphql, which was never a member, is not an error — SREM simply returns 0 because zero members matched.

Example 3: Unique daily visitor tracking

SADD visitors:2026-08-10 user:1001 user:1002 user:1003
SADD visitors:2026-08-10 user:1001
SCARD visitors:2026-08-10
SISMEMBER visitors:2026-08-10 user:1002
SISMEMBER visitors:2026-08-10 user:9999

Output:

(integer) 3
(integer) 0
(integer) 3
(integer) 1
(integer) 0

This is a realistic pattern: every time a user hits your site, you call SADD visitors:2026-08-10 user:<id>. Because SADD is idempotent for existing members, the same user visiting many times only ever counts once — the second SADD for user:1001 returns 0 and cardinality stays at 3. SCARD gives you the unique-visitor count in O(1) without pulling every member across the network, and SISMEMBER answers “was this specific user here today” in O(1) as well. In production you’d also call EXPIRE visitors:2026-08-10 172800 so old daily sets don’t accumulate forever.

How It Works Step by Step

When you run SADD key member: (1) Redis looks up key in the main keyspace dictionary; (2) if the key doesn’t exist, a new empty set object is created, starting in the most compact encoding possible (intset if the member is an integer, listpack otherwise); (3) the member is inserted into the underlying structure, which first checks for an existing equal member to preserve uniqueness; (4) if the set has grown past the configured size threshold, or a non-integer value was added to an intset, Redis converts the encoding to a hashtable; (5) the command returns the number of members that were newly added. All of this happens within a single, uninterruptible pass on the main thread, so no other client’s command can observe a partially-updated set.

SREM follows the mirror process: it locates each requested member in the underlying structure and deletes it if present, counting successful deletions. If the last member is removed, Redis frees the set object and deletes the key itself — there’s no such thing as a persisted empty set.

Common Mistakes

Mistake 1: Calling a set command on a key holding a different type

Every Redis key has exactly one data type. Using SADD on a key that already holds a string returns a WRONGTYPE error rather than silently converting it:

SET session:abc123 "active"
SADD session:abc123 "extra"

Output:

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

The fix is simply to use a distinct key name for the set (e.g. session:abc123:flags) rather than reusing a key that already stores a string.

Mistake 2: Assuming SMEMBERS preserves insertion order

Sets are unordered by definition. If you need a stable, sorted listing — a leaderboard, a “most recent” feed — a plain set is the wrong tool no matter how consistent the order looks in testing. Use a sorted set instead:

ZADD leaderboard:global 100 alice 200 bob
ZRANGE leaderboard:global 0 -1

Output:

(integer) 2
1) "alice"
2) "bob"

ZRANGE returns members sorted by score, which SMEMBERS can never guarantee.

Mistake 3: Using SMEMBERS on a very large set in production

Just as KEYS is dangerous on a large keyspace because it’s O(N) and blocks the single-threaded server for the entire scan, calling SMEMBERS on a set with millions of members pulls every single one across the network and through the server in one shot, tying up that command’s execution time and a large response buffer. For large sets, iterate incrementally instead with the cursor-based SSCAN, which fetches members in small batches without blocking:

SSCAN visitors:2026-08-10 0

Output:

1) "0"
2) (empty array)

Here the key doesn’t exist yet in a fresh keyspace, so the cursor immediately returns to 0 (meaning the scan is complete) with no members — on a populated set you’d pass the returned cursor back in on the next call until it returns to 0.

Best Practices

  • Rely on SADD‘s built-in uniqueness instead of doing a read-then-write “check if exists, then insert” in application code — the latter is a race condition under concurrency, while SADD is atomic.
  • Use SCARD to get a set’s size; never fetch the whole set with SMEMBERS just to count it.
  • For sets that can grow large (unbounded user activity, tags across a huge catalog), iterate with SSCAN rather than SMEMBERS.
  • Set a TTL with EXPIRE on sets that represent a time window (daily visitors, rate-limit windows) so they don’t accumulate forever and leak memory.
  • Use SISMEMBER for single-value membership checks instead of pulling the whole set and searching client-side — it’s O(1) versus O(N).
  • Use descriptive, colon-namespaced key names (tags:post:100, visitors:2026-08-10) so keys are self-documenting and easy to pattern-match later with SCAN.
  • Reach for a sorted set (ZADD) instead of a plain set the moment you need ordering, ranking, or range queries.

Practice Exercises

  • Create a set tags:post:200 with the tags python, tutorial, and beginner. Remove beginner, then add intermediate. Use SMEMBERS to confirm the final contents.
  • Simulate two users joining a chat room by adding user:alice and user:bob to a set called room:general:online. Use SISMEMBER to check whether user:carol is online, then have user:alice leave with SREM and confirm the room’s new member count with SCARD.
  • Given a raw list of email addresses with duplicates (a@x.com, b@x.com, a@x.com, c@x.com), add them all to a set called signups:dedup with a single SADD call and note how many were actually new versus duplicates within the call.

Summary

  • SADD key member [member ...] adds members to a set, ignoring ones already present, and returns the count of members newly added.
  • SMEMBERS key returns every member of a set, in no guaranteed order; avoid it on very large sets and use SSCAN instead.
  • SREM key member [member ...] removes members, returns the count actually removed, and silently no-ops on members that don’t exist.
  • Sets are unordered and enforce uniqueness natively; reach for a sorted set when order or ranking matters.
  • All three commands are O(1) per member and fully atomic thanks to Redis’s single-threaded execution model.
  • A set key stops existing the moment its last member is removed, and TTLs on set keys behave like TTLs on any other key.