As33
@periodic/
arsenic
redis_sdiffstore
⚠️ Warning

Set difference with write — SDIFF cost plus a write of the stored result

SDIFFSTORE computes the difference between sets and writes the result to a destination key. Like SUNIONSTORE and SINTERSTORE, its cost is the same as the corresponding non-store operation (O(N) across all input sets) plus a write. The correct pattern is to run this in a background worker and read the stored key on hot paths.

Common Causes

  • "Unseen items" or similar diff results recomputed per request
  • Inline SDIFFSTORE in request handlers where a background refresh would suffice
  • Cache invalidation patterns that store-then-read on every API call

How to Fix

  1. 1.Pre-compute the diff in a background worker and cache the result key with a TTL
  2. 2.Maintain the diff state incrementally at write time (SREM on seen, SADD on new items)
  3. 3.Use the destination key directly on hot paths — never recompute inline
  4. 4.Pair with SCARD to count without fetching all members when only the count is needed

Example

typescript
// BAD — diffstore called inline on every recommendation request
app.get('/api/recommendations', async (req, res) => {
  const dest = `unseen:${req.user.id}`;
  await redis.sdiffstore(dest, `recommendations:${req.user.id}`, `seen:${req.user.id}`);
  const unseen = await redis.srandmember(dest, 10);
  res.json(unseen);
});

// GOOD — maintain incrementally at write time
async function markSeen(redis: Redis, userId: string, itemId: string) {
  await redis.srem(`recommendations:${userId}`, itemId);
  await redis.sadd(`seen:${userId}`, itemId);
  // recommendations set is now always the live diff — no SDIFF needed
}

// Read directly — O(1) to count, O(log N) to sample
const count = await redis.scard(`recommendations:${userId}`);
const sample = await redis.srandmember(`recommendations:${userId}`, 10);

// GOOD — background pre-compute when write-time tracking is not feasible
async function refreshUnseenCache(redis: Redis, userId: string) {
  const dest = `unseen:cache:${userId}`;
  await redis.sdiffstore(dest, `recommendations:${userId}`, `seen:${userId}`);
  await redis.expire(dest, 180);
}

// Schedule refresh every 2 minutes per active user
// Hot path reads from the cached key, never calls SDIFFSTORE