As33
@periodic/
arsenic
redis_sinter
⚠️ Warning

Set intersection — O(N*M) in the worst case, expensive with large input sets

SINTER computes the intersection of multiple sets and returns only members present in all sets. Redis optimises by iterating the smallest set and checking membership in the others, but it is still O(N*M) worst case where N is the size of the smallest set and M is the number of input sets. On large, frequently overlapping sets called at request frequency this causes significant CPU and memory overhead.

Common Causes

  • Mutual friend or connection lookups computed live per request
  • Permission intersection checks across multiple roles or groups
  • Content filtering by intersecting multiple tag or category sets
  • Recommendation filtering across multiple user preference sets

How to Fix

  1. 1.Cache the intersection result with SINTERSTORE and a TTL
  2. 2.Move SINTER to a background job that refreshes the result periodically
  3. 3.Compute intersections in application memory when input sets are small enough to fetch cheaply
  4. 4.Restructure data to pre-compute intersections at write time

Example

typescript
// BAD — mutual friends computed on every profile visit
app.get('/api/users/:id/mutual', async (req, res) => {
  const mutual = await redis.sinter(
    `friends:${req.user.id}`,
    `friends:${req.params.id}`
  );
  res.json({ count: mutual.length, sample: mutual.slice(0, 5) });
});

// GOOD — cache the intersection
async function getMutualFriends(redis: Redis, userA: string, userB: string) {
  const cacheKey = `mutual:${[userA, userB].sort().join(':')}`;
  const cached = await redis.smembers(cacheKey);
  if (cached.length > 0) return cached;

  const mutual = await redis.sinterstore(cacheKey, `friends:${userA}`, `friends:${userB}`);
  await redis.expire(cacheKey, 300); // 5 minute TTL
  return redis.smembers(cacheKey);
}

// GOOD — application-level intersection on small bounded sets
const [setA, setB] = await Promise.all([
  redis.smembers(`tags:${itemA}`),
  redis.smembers(`tags:${itemB}`),
]);
const commonTags = setA.filter(tag => setB.includes(tag));

Sort keys consistently for cache hits

When caching an intersection of two sets, sort the key identifiers before joining them: [userA, userB].sort().join(':'). This ensures mutual:alice:bob and mutual:bob:alice hit the same cache key.