agentsclimarketplace

Redis patterns

Skill medy-gribkov/arcana/skills/redis-patterns

Universal AI development toolkit. 74 production-ready skills for every coding agent. Works with Claude Code, Cursor, Codex.

Install
npx -y skills add medy-gribkov/arcana --skill redis-patterns

Assembled 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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.