redis_zunionstoreSorted set union with aggregated scores — O(N) across all input sets plus a write
ZUNIONSTORE computes the union of multiple sorted sets and stores the result with aggregated scores. It is O(N) where N is the total number of elements across all input sorted sets, plus the cost of writing the result. Commonly used for combining ranked feeds, score aggregation, and multi-source ranking. When called synchronously on hot request paths rather than in background workers, it adds full union computation overhead to request latency.
Common Causes
- —Per-request ranked feed assembly combining multiple source sorted sets
- —Multi-source score aggregation (SUM/MAX) computed live for ranking
- —Inline ZUNIONSTORE in handlers that could use a cached pre-computed key
- —Frequent refresh of large union results without TTL-based scheduling
How to Fix
- 1.Pre-compute in a background worker using ZUNIONSTORE + EXPIRE on the destination key
- 2.Read from the destination key on hot paths — never compute inline
- 3.Use WEIGHTS option to tune score aggregation at pre-compute time rather than per-request
- 4.Invalidate and recompute only when source sets change, not on every read
Example
typescript
// BAD — union recomputed per feed request
app.get('/api/feed', async (req, res) => {
const dest = `feed:${req.user.id}:ranked`;
await redis.zunionstore(
dest, 3,
`engagement:${req.user.id}`,
'trending:global',
`personal:${req.user.id}`,
'WEIGHTS', 2, 1, 1.5 // engagement boosted
);
const feed = await redis.zrevrange(dest, 0, 19, 'WITHSCORES');
res.json(feed);
});
// GOOD — pre-computed ranked feed with background refresh
async function refreshRankedFeed(redis: Redis, userId: string) {
const dest = `feed:${userId}:ranked`;
await redis.zunionstore(
dest, 3,
`engagement:${userId}`,
'trending:global',
`personal:${userId}`,
'WEIGHTS', 2, 1, 1.5
);
await redis.expire(dest, 180); // 3 minute TTL
}
// Trigger on events that affect ranking
async function onNewPost(redis: Redis, authorId: string) {
await redis.zadd('trending:global', engagementScore, postId);
// Schedule feed refreshes for author's followers
const followers = await redis.smembers(`followers:${authorId}`);
for (const followerId of followers) {
queueJob('refresh-feed', { userId: followerId });
}
}
// Hot path is now O(log N + M) — reading from a pre-scored sorted set
app.get('/api/feed', async (req, res) => {
const feed = await redis.zrevrange(`feed:${req.user.id}:ranked`, 0, 19, 'WITHSCORES');
res.json(feed);
});WEIGHTS for personalised ranking
ZUNIONSTORE's WEIGHTS option lets you multiply each input set's scores before aggregation. Use this to implement personalised ranking at pre-compute time rather than post-processing in application code.