Redis & Caching
Redis is an in-memory key–value store famous for its speed — reads and writes in microseconds. Because it holds data in RAM, it's rarely the primary database; instead it's the workhorse for caching, sessions, rate-limit counters, queues, and pub/sub. This page covers connecting from Node, the core data types, the cache-aside pattern, expiration/TTL, and the pitfalls (stale data, the thundering herd) that caching introduces.
What Redis is good for
Use case | Why Redis |
|---|---|
Caching DB/API results | Sub-millisecond reads offload your database |
Sessions | Fast, shared across instances (sessions) |
Rate limiting | Atomic counters with expiry (rate limiting) |
Queues / jobs | Lists + BullMQ for background work |
Pub/Sub & real-time | Channels for fan-out messaging |
Leaderboards | Sorted sets ranked by score |
Connecting
npm install redis # or: npm install ioredis
redis.js — one client at startup
import { createClient } from 'redis'
export const redis = createClient({ url: process.env.REDIS_URL })
redis.on('error', (err) => console.error('Redis error', err))
await redis.connect() // ONCE at startup; reuse everywhereCore data types
// String (the workhorse — often JSON):
await redis.set('user:1', JSON.stringify(user))
const raw = await redis.get('user:1')
// Numbers — atomic increment (counters, rate limits):
await redis.incr('views:home')
// Hash — field/value map (a record without serializing the whole thing):
await redis.hSet('user:1', { name: 'Ada', age: '36' })
// List — queue/stack: Set — unique members: Sorted set — ranked:
await redis.lPush('jobs', 'send-email')
await redis.sAdd('online', 'user:1')
await redis.zAdd('leaderboard', { score: 100, value: 'user:1' })The cache-aside pattern
async function getUser(id) {
const key = `user:${id}`
// 1. Try the cache first:
const cached = await redis.get(key)
if (cached) return JSON.parse(cached) // cache HIT
// 2. Miss → read the source of truth:
const user = await db.users.findById(id)
if (!user) return null
// 3. Populate the cache with a TTL, then return:
await redis.set(key, JSON.stringify(user), { EX: 300 }) // expire in 5 min
return user
}Expiration (TTL)
await redis.set('key', 'val', { EX: 60 }) // expire in 60 seconds
await redis.expire('key', 60) // set/refresh TTL on an existing key
await redis.ttl('key') // seconds remaining (-1 = no expiry)
await redis.set('lock', '1', { NX: true, EX: 10 }) // set only if absent (locks)Cache invalidation — the hard part
async function updateUser(id, data) {
const user = await db.users.update(id, data)
await redis.del(`user:${id}`) // invalidate so the next read reloads
return user
}The thundering herd / cache stampede
What to cache — and what not to
Good: expensive query results, rarely-changing reference data, computed aggregates, rendered fragments, session data.
Be careful: per-user data (key by user; mind privacy), anything that must be perfectly fresh.
Don't: treat Redis as your only copy of data you can't recompute — it's a cache, not durable truth.
Cache the result, not the database connection; measure hit rate to confirm the cache is earning its keep.