Skip to main content
Bytes & Beyond

Redis Sets

Sets in Redis - perfect for unique items, relationships, memberships, and finding common elements.

What are Sets?

A Set is an unordered collection of unique items. No duplicates allowed. Perfect for tracking memberships, relationships, and finding common elements between groups.

Think of it as a collection of tags or group memberships that you can quickly check.

Basic Commands

SADD tags "javascript" "web" "programming"
SADD tags "javascript"    # Already there, ignored

SMEMBERS tags             # Get all items
# Returns: {"javascript", "web", "programming"}

SCARD tags                # Size: 3
SISMEMBER tags "web"      # Is "web" in the set? Yes (true)
SREM tags "web"           # Remove "web"
SPOP tags                 # Remove and return a random item

Tracking Unique Users

Count unique visitors without duplicates:

# Each day, track who visited
SADD visitors:2026-02-09 "user:1" "user:2" "user:3" "user:1"
SADD visitors:2026-02-09 "user:2"  # Already there, ignored

# How many unique visitors?
SCARD visitors:2026-02-09  # 3 (user:1 counted once)

Node.js (ioredis):

import Redis from 'ioredis';
const redis = new Redis();

// Track page visitor
async function trackVisitor(pageId, userId) {
  const today = new Date().toISOString().split('T')[0];
  const key = `page:${pageId}:visitors:${today}`;
  
  // SADD returns 1 if new, 0 if already exists
  const isNew = await redis.sadd(key, userId);
  await redis.expire(key, 86400 * 7); // Keep for 7 days
  
  return { isNewVisitor: isNew === 1 };
}

// Get unique visitor count
async function getUniqueVisitors(pageId, date) {
  const key = `page:${pageId}:visitors:${date}`;
  return await redis.scard(key);
}

// Check if user visited before
async function hasVisited(pageId, userId, date) {
  const key = `page:${pageId}:visitors:${date}`;
  return await redis.sismember(key, userId) === 1;
}

Python (redis-py):

import redis
from datetime import date

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

def track_visitor(page_id: int, user_id: str) -> dict:
    today = date.today().isoformat()
    key = f"page:{page_id}:visitors:{today}"
    
    # SADD returns 1 if new, 0 if already exists
    is_new = r.sadd(key, user_id)
    r.expire(key, 86400 * 7)  # Keep for 7 days
    
    return {'is_new_visitor': is_new == 1}

def get_unique_visitors(page_id: int, day: str = None) -> int:
    day = day or date.today().isoformat()
    key = f"page:{page_id}:visitors:{day}"
    return r.scard(key)

# Usage
track_visitor(1, "user:123")
count = get_unique_visitors(1)

Finding Common Items (SINTER)

Who are friends of both Alice and Bob?

SADD alice:friends "bob" "charlie" "diana" "eve"
SADD bob:friends "alice" "charlie" "frank" "grace"

# Find mutual friends
SINTER alice:friends bob:friends
# Result: {"charlie"}

# Get list and store it
SINTERSTORE mutual_friends alice:friends bob:friends
# Now mutual_friends = {"charlie"}

Node.js (ioredis):

// Find mutual friends
async function getMutualFriends(userId1, userId2) {
  return await redis.sinter(
    `user:${userId1}:friends`,
    `user:${userId2}:friends`
  );
}

// Find common interests for recommendations
async function getCommonInterests(userId1, userId2) {
  const common = await redis.sinter(
    `user:${userId1}:interests`,
    `user:${userId2}:interests`
  );
  return common;
}

// Friend suggestions based on mutual connections
async function getFriendSuggestions(userId) {
  const friends = await redis.smembers(`user:${userId}:friends`);
  const suggestions = new Set();
  
  for (const friendId of friends) {
    // Get friends of friends
    const fof = await redis.sdiff(
      `user:${friendId}:friends`,
      `user:${userId}:friends`
    );
    fof.forEach(f => f !== userId && suggestions.add(f));
  }
  
  return Array.from(suggestions).slice(0, 10);
}

Python (redis-py):

def get_mutual_friends(user_id: str, other_id: str) -> list:
    return list(r.sinter(
        f"user:{user_id}:friends",
        f"user:{other_id}:friends"
    ))

def get_common_interests(user_id: str, other_id: str) -> list:
    return list(r.sinter(
        f"user:{user_id}:interests",
        f"user:{other_id}:interests"
    ))

def get_friend_suggestions(user_id: str, limit: int = 10) -> list:
    friends = r.smembers(f"user:{user_id}:friends")
    suggestions = set()
    
    for friend_id in friends:
        # Friends of friends, excluding current friends
        fof = r.sdiff(
            f"user:{friend_id}:friends",
            f"user:{user_id}:friends"
        )
        suggestions.update(f for f in fof if f != user_id)
    
    return list(suggestions)[:limit]

# Usage
mutual = get_mutual_friends("alice", "bob")
common = get_common_interests("user:1", "user:2")
suggestions = get_friend_suggestions("user:1")

Union - All Items from Multiple Sets (SUNION)

Combine multiple groups:

SADD alice:friends "bob" "charlie" "diana"
SADD bob:friends "charlie" "frank"
SADD charlie:friends "alice" "diana" "eve"

# All their friends (combined)
SUNION alice:friends bob:friends charlie:friends
# Result: {"bob", "charlie", "diana", "frank", "alice", "eve"}

Real-world use:

# Notify all users who follow these topics
SADD followers:javascript "user:1" "user:2" "user:5"
SADD followers:nodejs "user:2" "user:3" "user:5"

# New JavaScript article posted, notify everyone
SUNION followers:javascript followers:nodejs
# Get all unique followers

Difference - Items Only in One Set (SDIFF)

Find items that are in one set but not in others:

SADD alice:friends "bob" "charlie" "diana" "eve"
SADD bob:friends "alice" "charlie" "frank"

# Who are Alice's friends that Bob isn't friends with?
SDIFF alice:friends bob:friends
# Result: {"diana", "eve"}

Real-world use:

# Find features this user hasn't explored yet
SADD all:features "profile" "settings" "notifications" "analytics" "billing"
SADD user:1:explored "profile" "settings"

# What's new for them to discover?
SDIFF all:features user:1:explored
# Result: {"notifications", "analytics", "billing"}

Tagging System

Use Sets for flexible tagging:

# Add tags to articles
SADD article:1:tags "redis" "caching" "performance"
SADD article:2:tags "redis" "streams" "messaging"
SADD article:3:tags "caching" "database"

# Articles with "redis" tag
SADD tag:redis:articles "article:1" "article:2"

# Find articles with BOTH "redis" AND "caching"
SINTER tag:redis:articles tag:caching:articles
# Result: {"article:1"}

# OR - find articles with EITHER tag
SUNION tag:redis:articles tag:caching:articles
# Result: {"article:1", "article:2", "article:3"}

Node.js (ioredis):

// Add tags to an article
async function tagArticle(articleId, tags) {
  const pipeline = redis.pipeline();
  
  // Add tags to article's tag set
  pipeline.sadd(`article:${articleId}:tags`, ...tags);
  
  // Add article to each tag's article set
  for (const tag of tags) {
    pipeline.sadd(`tag:${tag}:articles`, `article:${articleId}`);
  }
  
  await pipeline.exec();
}

// Find articles by multiple tags (AND)
async function findArticlesByAllTags(tags) {
  const keys = tags.map(t => `tag:${t}:articles`);
  return await redis.sinter(...keys);
}

// Find articles by any tag (OR)
async function findArticlesByAnyTag(tags) {
  const keys = tags.map(t => `tag:${t}:articles`);
  return await redis.sunion(...keys);
}

// Usage
await tagArticle(1, ['redis', 'caching', 'performance']);
const articles = await findArticlesByAllTags(['redis', 'caching']);

Python (redis-py):

def tag_article(article_id: int, tags: list[str]):
    pipe = r.pipeline()
    
    # Add tags to article's tag set
    pipe.sadd(f"article:{article_id}:tags", *tags)
    
    # Add article to each tag's article set
    for tag in tags:
        pipe.sadd(f"tag:{tag}:articles", f"article:{article_id}")
    
    pipe.execute()

def find_articles_by_tags(tags: list[str], match_all: bool = True) -> list:
    keys = [f"tag:{t}:articles" for t in tags]
    
    if match_all:  # AND - must have all tags
        return list(r.sinter(*keys))
    else:  # OR - any of the tags
        return list(r.sunion(*keys))

# Usage
tag_article(1, ['redis', 'caching', 'performance'])
articles = find_articles_by_tags(['redis', 'caching'], match_all=True)

Blocking Social Network

Block users and check quickly:

# User blocks these people
SADD user:1:blocked "user:2" "user:5" "user:10"

# Can they message each other?
if SISMEMBER user:1:blocked "user:2"
  REJECT("User blocked")

Deduplication

Remove duplicates from a list:

# Get all items and remove duplicates
SADD unique_items "apple" "banana" "apple" "orange" "banana"

SMEMBERS unique_items
# Result: {"apple", "banana", "orange"}

Permissions and Roles

Track what a user can do:

# User 1 has these permissions
SADD user:1:permissions "read" "write" "delete" "admin"

# Check permission
if SISMEMBER user:1:permissions "admin"
  allow_admin_panel()

Node.js (ioredis):

// Check single permission
async function hasPermission(userId, permission) {
  return await redis.sismember(`user:${userId}:permissions`, permission) === 1;
}

// Check multiple permissions (all required)
async function hasAllPermissions(userId, permissions) {
  const userPerms = `user:${userId}:permissions`;
  for (const perm of permissions) {
    if (await redis.sismember(userPerms, perm) !== 1) {
      return false;
    }
  }
  return true;
}

// Middleware for Express/Fastify
function requirePermission(...permissions) {
  return async (req, res, next) => {
    const userId = req.user.id;
    const hasAll = await hasAllPermissions(userId, permissions);
    
    if (!hasAll) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

// Usage
app.delete('/users/:id', requirePermission('admin', 'delete'), deleteUser);

Python (redis-py):

def has_permission(user_id: str, permission: str) -> bool:
    return r.sismember(f"user:{user_id}:permissions", permission) == 1

def has_all_permissions(user_id: str, permissions: list[str]) -> bool:
    user_perms = f"user:{user_id}:permissions"
    return all(r.sismember(user_perms, p) for p in permissions)

def require_permissions(user_id: str, permissions: list[str]):
    """Raises exception if user lacks required permissions"""
    if not has_all_permissions(user_id, permissions):
        raise PermissionError(f"User {user_id} lacks required permissions")

# Usage
if has_permission("user:1", "admin"):
    print("User is admin")

# Check multiple permissions
if has_all_permissions("user:1", ["admin", "delete"]):
    delete_user(target_id)

Sampling

Pick random items:

SADD users "user:1" "user:2" "user:3" "user:4" "user:5"

# Pick a random user
SPOP users
# Returns one random user, removes it

# Pick 3 random users without removing
SRANDMEMBER users 3
# Returns 3 random users

Real-world use:

# Random A/B test selection
SADD experiment:control "user:1" "user:2" "user:3"
SRANDMEMBER experiment:control 1  # Pick random for control

Common Patterns

Follow/Follower System

# Alice follows these people
SADD alice:following "bob" "charlie" "diana"

# Bob's followers
SADD bob:followers "alice" "eve" "frank"

# Check if Alice follows Bob
SISMEMBER alice:following "bob"  # Yes

Node.js (ioredis):

// Follow a user
async function followUser(followerId, followeeId) {
  const pipeline = redis.pipeline();
  
  pipeline.sadd(`user:${followerId}:following`, followeeId);
  pipeline.sadd(`user:${followeeId}:followers`, followerId);
  
  await pipeline.exec();
  return { success: true };
}

// Unfollow a user
async function unfollowUser(followerId, followeeId) {
  const pipeline = redis.pipeline();
  
  pipeline.srem(`user:${followerId}:following`, followeeId);
  pipeline.srem(`user:${followeeId}:followers`, followerId);
  
  await pipeline.exec();
  return { success: true };
}

// Check if following
async function isFollowing(followerId, followeeId) {
  return await redis.sismember(`user:${followerId}:following`, followeeId) === 1;
}

// Get follower/following counts
async function getFollowStats(userId) {
  const [followers, following] = await Promise.all([
    redis.scard(`user:${userId}:followers`),
    redis.scard(`user:${userId}:following`)
  ]);
  return { followers, following };
}

Python (redis-py):

def follow_user(follower_id: str, followee_id: str):
    pipe = r.pipeline()
    pipe.sadd(f"user:{follower_id}:following", followee_id)
    pipe.sadd(f"user:{followee_id}:followers", follower_id)
    pipe.execute()

def unfollow_user(follower_id: str, followee_id: str):
    pipe = r.pipeline()
    pipe.srem(f"user:{follower_id}:following", followee_id)
    pipe.srem(f"user:{followee_id}:followers", follower_id)
    pipe.execute()

def is_following(follower_id: str, followee_id: str) -> bool:
    return r.sismember(f"user:{follower_id}:following", followee_id) == 1

def get_follow_stats(user_id: str) -> dict:
    return {
        'followers': r.scard(f"user:{user_id}:followers"),
        'following': r.scard(f"user:{user_id}:following")
    }

# Usage
follow_user("alice", "bob")
print(is_following("alice", "bob"))  # True
print(get_follow_stats("bob"))  # {'followers': 1, 'following': 0}

Feature Flag Groups

# Which users get the new feature?
SADD feature:new_ui:beta_users "user:1" "user:5" "user:10"

# Is this user in beta?
if SISMEMBER feature:new_ui:beta_users user_id
  show_new_ui()

Languages/Skills

# What languages does each developer know?
SADD dev:alice:languages "javascript" "python" "go"
SADD dev:bob:languages "javascript" "ruby" "java"

# Find developers who know Go
SADD language:go:developers "dev:alice" "dev:charlie"