Apply production Redis patterns including caching strategies, rate limiting, distributed locks, pub/sub, streams, and memory optimization with TypeScript and Go examples
Production patterns for Redis covering caching, session management, rate limiting, distributed systems, and performance optimization.
BAD: No TTL, cache stampede vulnerability
// ioredis
import Redis from 'ioredis';
const redis = new Redis();
async function getUser(id: string) {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
await redis.set(`user:${id}`, JSON.stringify(user)); // No TTL!
return user;
}
GOOD: TTL + stampede protection with locking
async function getUser(id: string) {
const key = `user:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
// Acquire lock to prevent stampede
const lockKey = `lock:${key}`;
const acquired = await redis.set(lockKey, '1', 'NX', 'EX', 10);
if (!acquired) {
// Wait and retry if another process is loading
await new Promise(resolve => setTimeout(resolve, 100));
return getUser(id);
}
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
await redis.setex(key, 3600, JSON.stringify(user)); // 1 hour TTL
return user;
} finally {
await redis.del(lockKey);
}
}
BAD: Cache and DB out of sync
// go-redis
import "github.com/redis/go-redis/v9"
func updateUser(ctx context.Context, rdb *redis.Client, user User) error {
data, _ := json.Marshal(user)
rdb.Set(ctx, fmt.Sprintf("user:%s", user.ID), data, 0)
return db.Exec("UPDATE users SET name = $1 WHERE id = $2", user.Name, user.ID)
// If DB fails, cache is dirty!
}
GOOD: Write DB first, then invalidate cache
func updateUser(ctx context.Context, rdb *redis.Client, user User) error {
// Write to DB first
if err := db.Exec("UPDATE users SET name = $1 WHERE id = $2", user.Name, user.ID); err != nil {
return err
}
// Invalidate cache, don't care if this fails
key := fmt.Sprintf("user:%s", user.ID)
rdb.Del(ctx, key)
return nil
}
BAD: Fixed window allows burst
async function isRateLimited(userId: string): Promise<boolean> {
const key = `ratelimit:${userId}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(key);
await redis.expire(key, 60);
return count > 100; // 100/min but allows 200 across window boundary
}
GOOD: Sliding window with sorted set
async function isRateLimited(userId: string, limit = 100, windowSec = 60): Promise<boolean> {
const key = `ratelimit:${userId}`;
const now = Date.now();
const windowStart = now - (windowSec * 1000);
const pipe = redis.pipeline();
pipe.zremrangebyscore(key, 0, windowStart); // Remove old entries
pipe.zadd(key, now, `${now}-${Math.random()}`); // Add current request
pipe.zcard(key); // Count requests in window
pipe.expire(key, windowSec);
const results = await pipe.exec();
const count = results[2][1] as number;
return count > limit;
}
BAD: Storing entire session object, no refresh
func saveSession(ctx context.Context, rdb *redis.Client, session Session) error {
data, _ := json.Marshal(session)
return rdb.Set(ctx, session.ID, data, 30*time.Minute).Err()
// Session expires even if user is active!
}
GOOD: Hash with TTL refresh on access
func saveSession(ctx context.Context, rdb *redis.Client, sessionID string, fields map[string]interface{}) error {
key := fmt.Sprintf("session:%s", sessionID)
// Store as hash for partial updates
for field, value := range fields {
val, _ := json.Marshal(value)
if err := rdb.HSet(ctx, key, field, val).Err(); err != nil {
return err
}
}
// Refresh TTL on every access
return rdb.Expire(ctx, key, 30*time.Minute).Err()
}
func getSessionField(ctx context.Context, rdb *redis.Client, sessionID, field string) (string, error) {
key := fmt.Sprintf("session:%s", sessionID)
val, err := rdb.HGet(ctx, key, field).Result()
if err != nil {
return "", err
}
// Refresh TTL on read
rdb.Expire(ctx, key, 30*time.Minute)
return val, nil
}
BAD: No timeout, no fencing token
async function acquireLock(resource: string): Promise<boolean> {
const acquired = await redis.setnx(`lock:${resource}`, '1');
return acquired === 1; // Held forever if process crashes!
}
GOOD: Lock with timeout and fencing token
import { randomBytes } from 'crypto';
interface Lock {
token: string;
resource: string;
}
async function acquireLock(resource: string, ttlMs = 10000): Promise<Lock | null> {
const token = randomBytes(16).toString('hex');
const key = `lock:${resource}`;
const acquired = await redis.set(key, token, 'PX', ttlMs, 'NX');
if (!acquired) return null;
return { token, resource };
}
async function releaseLock(lock: Lock): Promise<boolean> {
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
const result = await redis.eval(script, 1, `lock:${lock.resource}`, lock.token);
return result === 1;
}
GOOD: Publisher with fanout
func publishEvent(ctx context.Context, rdb *redis.Client, channel string, event interface{}) error {
data, err := json.Marshal(event)
if err != nil {
return err
}
return rdb.Publish(ctx, channel, data).Err()
}
GOOD: Subscriber with graceful shutdown
func subscribe(ctx context.Context, rdb *redis.Client, channel string) {
pubsub := rdb.Subscribe(ctx, channel)
defer pubsub.Close()
ch := pubsub.Channel()
for {
select {
case msg := <-ch:
var event Event
if err := json.Unmarshal([]byte(msg.Payload), &event); err != nil {
log.Printf("decode error: %v", err)
continue
}
handleEvent(event)
case <-ctx.Done():
return
}
}
}
GOOD: Leaderboard with score and rank
async function updateScore(userId: string, score: number) {
await redis.zadd('leaderboard', score, userId);
}
async function getTopPlayers(limit = 10): Promise<Array<{userId: string, score: number, rank: number}>> {
const results = await redis.zrevrange('leaderboard', 0, limit - 1, 'WITHSCORES');
const players = [];
for (let i = 0; i < results.length; i += 2) {
players.push({
userId: results[i],
score: parseFloat(results[i + 1]),
rank: Math.floor(i / 2) + 1
});
}
return players;
}
async function getUserRank(userId: string): Promise<{rank: number, score: number} | null> {
const [rank, score] = await Promise.all([
redis.zrevrank('leaderboard', userId),
redis.zscore('leaderboard', userId)
]);
if (rank === null) return null;
return { rank: rank + 1, score: parseFloat(score) };
}
GOOD: Append events and consume with consumer groups
async function appendEvent(stream: string, event: Record<string, string>) {
await redis.xadd(stream, '*', ...Object.entries(event).flat());
}
async function consumeEvents(stream: string, group: string, consumer: string) {
// Create consumer group if not exists
try {
await redis.xgroup('CREATE', stream, group, '0', 'MKSTREAM');
} catch (err) {
// Group already exists
}
while (true) {
const results = await redis.xreadgroup(
'GROUP', group, consumer,
'BLOCK', 5000,
'COUNT', 10,
'STREAMS', stream, '>'
);
if (!results || results.length === 0) continue;
for (const [streamName, entries] of results) {
for (const [id, fields] of entries) {
const event = Object.fromEntries(
fields.reduce((acc, val, i, arr) => {
if (i % 2 === 0) acc.push([val, arr[i + 1]]);
return acc;
}, [])
);
await processEvent(event);
await redis.xack(stream, group, id);
}
}
}
}
BAD: Inconsistent, hard to debug
await redis.set('user_123', data);
await redis.set('User:456', data);
await redis.set('u:789', data);
GOOD: Hierarchical, predictable
// Pattern: {resource}:{id}:{field}
await redis.set('user:123:profile', profileData);
await redis.set('user:123:settings', settingsData);
await redis.set('session:abc:user', userId);
await redis.zadd('leaderboard:daily:2024-03-02', score, userId);
BAD: Large blobs in cache
func cacheDocument(ctx context.Context, rdb *redis.Client, docID string, doc []byte) error {
// Storing 10MB PDFs in Redis!
return rdb.Set(ctx, fmt.Sprintf("doc:%s", docID), doc, time.Hour).Err()
}
GOOD: Store reference, maxmemory-policy
// redis.conf: maxmemory 2gb, maxmemory-policy allkeys-lru
func cacheDocument(ctx context.Context, rdb *redis.Client, docID string, s3Key string) error {
// Store S3 reference only
meta := map[string]interface{}{
"s3_key": s3Key,
"cached_at": time.Now().Unix(),
}
data, _ := json.Marshal(meta)
return rdb.Set(ctx, fmt.Sprintf("doc:%s", docID), data, time.Hour).Err()
}
BAD: New connection per request
async function getKey(key: string) {
const redis = new Redis(); // New connection every time!
const value = await redis.get(key);
await redis.quit();
return value;
}
GOOD: Reuse connection pool
// Initialize once
const redis = new Redis({
host: 'localhost',
port: 6379,
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 50, 2000)
});
async function getKey(key: string) {
return redis.get(key); // Reuses connection
}
BAD: Sequential round trips
func getUserData(ctx context.Context, rdb *redis.Client, userID string) (User, error) {
profile, _ := rdb.Get(ctx, fmt.Sprintf("user:%s:profile", userID)).Result()
settings, _ := rdb.Get(ctx, fmt.Sprintf("user:%s:settings", userID)).Result()
posts, _ := rdb.LLen(ctx, fmt.Sprintf("user:%s:posts", userID)).Result()
// 3 round trips!
return User{Profile: profile, Settings: settings, PostCount: posts}, nil
}
GOOD: Single round trip with pipeline
func getUserData(ctx context.Context, rdb *redis.Client, userID string) (User, error) {
pipe := rdb.Pipeline()
profileCmd := pipe.Get(ctx, fmt.Sprintf("user:%s:profile", userID))
settingsCmd := pipe.Get(ctx, fmt.Sprintf("user:%s:settings", userID))
postsCmd := pipe.LLen(ctx, fmt.Sprintf("user:%s:posts", userID))
_, err := pipe.Exec(ctx)
if err != nil {
return User{}, err
}
return User{
Profile: profileCmd.Val(),
Settings: settingsCmd.Val(),
PostCount: postsCmd.Val(),
}, nil
}
GOOD: Atomic operations with WATCH
async function transferPoints(fromUser: string, toUser: string, points: number) {
const fromKey = `user:${fromUser}:points`;
const toKey = `user:${toUser}:points`;
while (true) {
await redis.watch(fromKey);
const balance = parseInt(await redis.get(fromKey) || '0');
if (balance < points) {
await redis.unwatch();
throw new Error('Insufficient points');
}
const multi = redis.multi();
multi.decrby(fromKey, points);
multi.incrby(toKey, points);
const result = await multi.exec();
if (result) break; // Success
// Transaction failed due to concurrent modification, retry
}
}
Use Redis for:
Avoid Redis for: