Examples
A small gallery of Redis patterns you reach for in real apps: rate limiting, distributed lock, pub/sub, leaderboard, session store, and cached aggregate. Each is short enough to drop into a service today.
Six Redis patterns with working code
EXAMPLE
// npm i ioredis
import Redis from 'ioredis';
const r = new Redis(process.env.REDIS_URL);
// ===== 1) Rate limiting: fixed window, 60 req / 60s per IP =====
async function allow(ip: string): Promise<boolean> {
const key = \`rl:${ip}:${Math.floor(Date.now() / 60_000)}\`;
const count = await r.incr(key);
if (count === 1) await r.expire(key, 60);
return count <= 60;
}
// ===== 2) Sliding-window rate limit (more accurate, slightly costlier) =====
async function allowSliding(userId: string, max = 30, windowMs = 60_000) {
const now = Date.now();
const key = \`slide:${userId}\`;
const pipe = r.multi();
pipe.zadd(key, now, \`${now}-${Math.random()}\`);
pipe.zremrangebyscore(key, 0, now - windowMs);
pipe.zcard(key);
pipe.pexpire(key, windowMs);
const res: any = await pipe.exec();
return res[2][1] <= max;
}
// ===== 3) Distributed lock (single-instance Redis, not Redlock) =====
async function withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>) {
const token = crypto.randomUUID();
// NX = set only if not exists; PX = TTL in ms
const ok = await r.set('lock:' + key, token, 'PX', ttlMs, 'NX');
if (!ok) throw new Error('lock busy');
try {
return await fn();
} finally {
// Release only if we still own it — Lua keeps it atomic
await r.eval(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
1, 'lock:' + key, token,
);
}
}
// ===== 4) Pub/Sub for fan-out events =====
const pub = new Redis(process.env.REDIS_URL);
const sub = new Redis(process.env.REDIS_URL);
sub.subscribe('orders:created');
sub.on('message', (channel, msg) => console.log(channel, JSON.parse(msg)));
await pub.publish('orders:created', JSON.stringify({ id: 'o1', total: 49.95 }));
// ===== 5) Sorted-set leaderboard =====
await r.zincrby('leaderboard:weekly', 100, 'alice');
await r.zincrby('leaderboard:weekly', 250, 'bob');
const top = await r.zrevrange('leaderboard:weekly', 0, 9, 'WITHSCORES');
// ===== 6) Session store — a hash per session id, with sliding TTL =====
async function loadSession(sid: string) {
const data = await r.hgetall('sess:' + sid);
if (!Object.keys(data).length) return null;
await r.expire('sess:' + sid, 60 * 60 * 24 * 14); // slide TTL on read
return data;
}
async function saveSession(sid: string, patch: Record<string, string>) {
await r.hmset('sess:' + sid, patch);
await r.expire('sess:' + sid, 60 * 60 * 24 * 14);
}
// ===== 7) Cached aggregate with double-checked locking =====
async function dailyRevenue(date: string): Promise<number> {
const key = \`rev:${date}\`;
const cached = await r.get(key);
if (cached) return Number(cached);
return withLock(key, 5000, async () => {
const recheck = await r.get(key);
if (recheck) return Number(recheck);
const rev = await heavyDbQuery(date);
await r.set(key, rev, 'EX', 300);
return rev;
});
}
async function heavyDbQuery(_d: string) { return 12345; }
Why it matters
A single-instance Redis lock (SET NX PX + Lua release) is the right tool 95% of the time. Reach for Redlock only when a multi-master Redis topology actually exists in production — the added complexity is rarely worth it when the simpler primitive plus a sensible TTL covers your real failure modes.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…