Redis patterns
Universal AI development toolkit. 74 production-ready skills for every coding agent. Works with Claude Code, Cursor, Codex.
npx -y skills add medy-gribkov/arcana --skill redis-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Apply production Redis patterns including caching strategies, rate limiting, distributed locks, pub/sub, streams, and memory optimization with TypeScript and Go examples
SKILL.md
12.5 KB, ~3.2k tokens by cl100k_base, as published. Nobody here has run it
Redis Patterns
Production patterns for Redis covering caching, session management, rate limiting, distributed systems, and performance optimization.
Cache-Aside Pattern
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);
}
}
Write-Through Cache
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
}
Sliding Window Rate Limiter
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;
}
Session Storage
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
}
Distributed Lock (Redlock)
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;
}
Pub/Sub for Real-Time Events
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
}
}
}
Sorted Sets for Leaderboards
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) };
}
Streams for Event Sourcing
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);
}
}
}
}
Key Naming Conventions
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);
Memory Optimization
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()
}
Connection Pooling
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
}
Pipeline for Batching
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
}
MULTI/EXEC for Transactions
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
}
}
When to Use Redis
Use Redis for:
- Caching frequently accessed data with TTL
- Session storage with automatic expiration
- Rate limiting and quota management
- Real-time features (pub/sub, leaderboards)
- Distributed locks for coordination
- Event streams with consumer groups
- Temporary data that can be reconstructed
Avoid Redis for:
- Primary data storage (use PostgreSQL)
- Large binary objects (use S3)
- Complex queries and joins (use SQL)
- Strong consistency requirements (Redis is eventually consistent across replicas)
- Audit logs requiring durability (Redis prioritizes speed over persistence)
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.