| name | redis |
| description | Redis patterns: caching, sessions, rate limiting, pub/sub, queues, distributed locks — with Python and Node.js examples |
Redis Skill
When to activate
- Caching expensive database queries or API responses
- Storing user sessions server-side
- Implementing rate limiting on API endpoints
- Building a job queue (BullMQ, Celery, RQ)
- Real-time features with pub/sub (notifications, chat)
- Distributed locks across multiple servers
- Leaderboards and sorted sets
When NOT to use
- Permanent data storage — Redis is in-memory, data loss is possible
- Complex relational queries — use PostgreSQL
- Large binary files — use S3/object storage
- When you just need a simple in-memory cache within one process — use an LRU dict
Instructions
Connection setup
Node.js (ioredis):
import Redis from 'ioredis'
const redis = new Redis(process.env.REDIS_URL!)
const redis = new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 50, 2000),
})
Python (redis-py):
import redis
r = redis.Redis.from_url(os.environ["REDIS_URL"], decode_responses=True)
pool = redis.ConnectionPool.from_url(os.environ["REDIS_URL"], max_connections=20)
r = redis.Redis(connection_pool=pool, decode_responses=True)
Caching patterns
Simple cache-aside (most common):
async function getCachedUser(userId: string): Promise<User> {
const key = `user:${userId}`
const cached = await redis.get(key)
if (cached) return JSON.parse(cached)
const user = await db.user.findUnique({ where: { id: userId } })
if (!user) throw new NotFoundError()
await redis.setex(key, 300, JSON.stringify(user))
return user
}
async function updateUser(userId: string, data: Partial<User>) {
const user = await db.user.update({ where: { id: userId }, data })
await redis.del()
user
}
Cache with stale-while-revalidate:
async function getCached<T>(
key: string,
ttlSec: number,
fetch: () => Promise<T>
): Promise<T> {
const cached = await redis.get(key)
if (cached) {
const ttl = await redis.ttl(key)
if (ttl < ttlSec * 0.2) {
fetch().then(data => redis.setex(key, ttlSec, JSON.stringify(data)))
}
return JSON.parse(cached)
}
const data = await fetch()
await redis.setex(key, ttlSec, JSON.stringify(data))
return data
}
Batch get (pipeline):
const keys = userIds.map(id => `user:${id}`)
const pipeline = redis.pipeline()
keys.forEach(key => pipeline.get(key))
const results = await pipeline.exec()
const users = results.map((result, i) =>
result[1] ? JSON.parse(result[1] as string) : null
)
Sessions
import session from 'express-session'
import { RedisStore } from 'connect-redis'
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
},
}))
async function invalidateToken(jti: string, expiresAt: number) {
const ttl = expiresAt - Math.floor(Date.now() / 1000)
if (ttl > 0) await redis.(, ttl, )
}
(): <> {
( redis.()) ===
}
Rate limiting
async function rateLimit(
identifier: string,
limit: number,
windowSec: number
): Promise<{ allowed: boolean; remaining: number; reset: number }> {
const key = `ratelimit:${identifier}`
const now = Date.now()
const windowMs = windowSec * 1000
const pipeline = redis.pipeline()
pipeline.zremrangebyscore(key, 0, now - windowMs)
pipeline.zadd(key, now, `${now}-${Math.random()}`)
pipeline.zcard(key)
pipeline.expire(key, windowSec)
const results = await pipeline.exec()
const count = results![2][1] as
{
: count <= limit,
: .(, limit - count),
: .((now + windowMs) / ),
}
}
app.( (req, res, next) => {
{ allowed, remaining, reset } = (req., , )
res.(, remaining)
res.(, reset)
(!allowed) res.().({ : })
()
})
Pub/Sub
const publisher = new Redis(process.env.REDIS_URL!)
async function publishEvent(channel: string, data: object) {
await publisher.publish(channel, JSON.stringify(data))
}
const subscriber = new Redis(process.env.REDIS_URL!)
await subscriber.subscribe('order-updates')
subscriber.on('message', (channel, message) => {
const event = JSON.parse(message)
console.log(`Event on ${channel}:`, event)
})
await subscriber.psubscribe('user:*:notifications')
subscriber.on('pmessage', (pattern, channel, message) => {
const userId = channel.split(':')[]
(userId, .(message))
})
Job queues (BullMQ)
import { Queue, Worker } from 'bullmq'
const emailQueue = new Queue('emails', {
connection: { host: process.env.REDIS_HOST, port: 6379 },
})
await emailQueue.add('welcome-email', {
to: 'user@example.com',
template: 'welcome',
}, {
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
delay: 5000,
})
const worker = new Worker('emails', async (job) => {
await sendEmail(job.data.to, job.data.template)
}, {
connection: { host: process.env.REDIS_HOST, port: 6379 },
concurrency: 5,
})
worker.(, {
.(, err)
})
Distributed locks
async function withLock<T>(
key: string,
ttlMs: number,
fn: () => Promise<T>
): Promise<T | null> {
const lockId = crypto.randomUUID()
const acquired = await redis.set(
`lock:${key}`, lockId,
'PX', ttlMs,
'NX',
)
if (!acquired) return null
try {
return await fn()
} finally {
await redis.eval(
`if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else return 0 end`,
1, `lock:${key}`, lockId
)
}
}
const result = await withLock(`payment:${webhookId}`, 30000, async () => {
return processPaymentWebhook(webhookId)
})
(result === ) .()
Sorted sets (leaderboards)
await redis.zadd('leaderboard', { score: 1250, member: userId })
await redis.zincrby('leaderboard', 50, userId)
const top10 = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES')
const rank = await redis.zrevrank('leaderboard', userId)
const score = await redis.zscore('leaderboard', userId)
Example
User: Add Redis caching to a FastAPI app with rate limiting (100 req/min per IP) and a BullMQ email queue.
Expected output:
redis.py — get_cached(), set_cached(), invalidate() helpers
middleware/rate_limit.py — sliding window rate limiter using ZADD
routes/users.py — cache-aside on GET /users/{id}, invalidate on PUT
queues/email_queue.ts (BullMQ) — producer + worker with retry/backoff