As33
@periodic/
arsenic
redis_sdiff
⚠️ Warning

Set difference — O(N) across all input sets, linear in total membership

SDIFF returns members present in the first set but not in any subsequent set. It is O(N) where N is the total number of elements across all sets. Common use cases include computing content a user has not yet seen, permissions that do not apply, or items not in a given category. When the first set is large, SDIFF transfers a large working set even if the final difference is small.

Common Causes

  • "Unseen" content or notification filters computed per request
  • Permission exclusion lists applied at read time
  • Recommendation deduplication — items seen minus items recommended
  • A/B test exclusion sets computed live

How to Fix

  1. 1.Cache the diff result with SDIFFSTORE and a TTL
  2. 2.Maintain the difference set explicitly at write time (SREM when items are seen)
  3. 3.Compute small diffs in application memory if sets are bounded
  4. 4.Move SDIFF to background workers for pre-computation

Example

typescript
// BAD — computing unseen items per request
app.get('/api/recommendations', async (req, res) => {
  const unseen = await redis.sdiff(
    `recommendations:${userId}`,
    `seen:${userId}`,
    `dismissed:${userId}`
  );
  res.json(unseen.slice(0, 10));
});

// GOOD — maintain seen/unseen state at write time
// When a user sees an item:
await redis.smove(`recommendations:${userId}`, `seen:${userId}`, itemId);
// Read unseen directly — no SDIFF needed
const unseen = await redis.sscan(`recommendations:${userId}`, '0', 'COUNT', 10);

// GOOD — cache the diff result
async function getUnseenItems(redis: Redis, userId: string) {
  const cacheKey = `unseen:${userId}`;
  let count = await redis.scard(cacheKey);

  if (count === 0) {
    await redis.sdiffstore(
      cacheKey,
      `recommendations:${userId}`,
      `seen:${userId}`
    );
    await redis.expire(cacheKey, 120);
    count = await redis.scard(cacheKey);
  }

  return redis.srandmember(cacheKey, 10);
}

// GOOD — application-level diff on small bounded sets
const [recs, seen] = await Promise.all([
  redis.smembers(`recommendations:${userId}`),
  redis.smembers(`seen:${userId}`),
]);
const seenSet = new Set(seen);
const unseen = recs.filter(id => !seenSet.has(id));