As33
@periodic/
arsenic
redis_sinterstore
⚠️ Warning

Set intersection with write — same cost as SINTER plus a write of the result

SINTERSTORE computes the intersection of multiple sets and stores the result in a destination key. It carries the same computational cost as SINTER (O(N*M)) plus the overhead of writing the result. When used correctly, this is a powerful caching primitive. When called on a request path rather than in a background worker, it adds the full intersection cost to every request plus write amplification.

Common Causes

  • Per-request mutual connection lookups that store and immediately read the result
  • Inline SINTERSTORE in API handlers that could pre-compute the value
  • Cache refresh logic triggered on every read rather than on TTL expiry

How to Fix

  1. 1.Run SINTERSTORE in a background job and set a TTL on the destination key
  2. 2.Use the stored key as the read target — never recompute synchronously on hot paths
  3. 3.Invalidate and recompute the stored key when the source sets change significantly
  4. 4.Consider write-time materialisation if intersection membership changes frequently

Example

typescript
// BAD — intersection computed and stored on every request
app.get('/api/users/:id/mutual', async (req, res) => {
  const dest = `mutual:${req.user.id}:${req.params.id}`;
  await redis.sinterstore(dest, `friends:${req.user.id}`, `friends:${req.params.id}`);
  const mutual = await redis.smembers(dest);
  res.json({ count: mutual.length });
});

// GOOD — pre-compute on friend graph changes, read cached key on requests
async function updateMutualCache(redis: Redis, userA: string, userB: string) {
  const sortedKey = [userA, userB].sort().join(':');
  const dest = `mutual:${sortedKey}`;
  await redis.sinterstore(dest, `friends:${userA}`, `friends:${userB}`);
  await redis.expire(dest, 600); // 10 minute cache
}

// Trigger on friend add/remove
async function addFriend(redis: Redis, userId: string, friendId: string) {
  await redis.sadd(`friends:${userId}`, friendId);
  await redis.sadd(`friends:${friendId}`, userId);
  // Invalidate related mutual caches in background
  queueJob('refresh-mutual-cache', { userId, friendId });
}

// Read is now O(1) — smembers on a small pre-computed set
app.get('/api/users/:id/mutual', async (req, res) => {
  const sortedKey = [req.user.id, req.params.id].sort().join(':');
  const mutual = await redis.smembers(`mutual:${sortedKey}`);
  res.json({ count: mutual.length });
});