Claude
Skills
Sign in
Back

redis-patterns

Included with Lifetime
$97 forever

Implements Redis patterns for caching, sessions, rate limiting, pub/sub, and distributed locks with best practices. Use when users request "Redis caching", "session storage", "rate limiter", "pub/sub messaging", or "distributed locks".

Backend & APIs

What this skill does


# Redis Patterns

Implement common Redis patterns for high-performance applications.

## Core Workflow

1. **Setup connection**: Configure Redis client
2. **Choose pattern**: Caching, sessions, queues, etc.
3. **Implement operations**: CRUD with proper TTL
4. **Handle errors**: Reconnection, fallbacks
5. **Monitor performance**: Memory, latency
6. **Optimize**: Pipelining, clustering

## Connection Setup

```typescript
// redis/client.ts
import { Redis } from 'ioredis';

// Single instance
export const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
  db: parseInt(process.env.REDIS_DB || '0'),

  // Connection options
  maxRetriesPerRequest: 3,
  retryStrategy(times) {
    const delay = Math.min(times * 50, 2000);
    return delay;
  },

  // Performance options
  enableReadyCheck: true,
  enableOfflineQueue: true,
  connectTimeout: 10000,

  // TLS for production
  tls: process.env.NODE_ENV === 'production' ? {} : undefined,
});

// Event handlers
redis.on('connect', () => console.log('Redis connecting...'));
redis.on('ready', () => console.log('Redis ready'));
redis.on('error', (err) => console.error('Redis error:', err));
redis.on('close', () => console.log('Redis connection closed'));

// Cluster connection
export const cluster = new Redis.Cluster([
  { host: 'redis-node-1', port: 6379 },
  { host: 'redis-node-2', port: 6379 },
  { host: 'redis-node-3', port: 6379 },
], {
  redisOptions: {
    password: process.env.REDIS_PASSWORD,
  },
  scaleReads: 'slave',
  maxRedirections: 16,
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await redis.quit();
});
```

## Caching Pattern

```typescript
// patterns/cache.ts
import { redis } from './client';

interface CacheOptions {
  ttl?: number;  // seconds
  prefix?: string;
}

export class Cache {
  private prefix: string;
  private defaultTTL: number;

  constructor(options: CacheOptions = {}) {
    this.prefix = options.prefix || 'cache:';
    this.defaultTTL = options.ttl || 3600;
  }

  private key(key: string): string {
    return `${this.prefix}${key}`;
  }

  async get<T>(key: string): Promise<T | null> {
    const data = await redis.get(this.key(key));
    if (!data) return null;

    try {
      return JSON.parse(data) as T;
    } catch {
      return data as unknown as T;
    }
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    const serialized = typeof value === 'string'
      ? value
      : JSON.stringify(value);

    await redis.setex(this.key(key), ttl || this.defaultTTL, serialized);
  }

  async getOrSet<T>(
    key: string,
    fetcher: () => Promise<T>,
    ttl?: number
  ): Promise<T> {
    const cached = await this.get<T>(key);
    if (cached !== null) return cached;

    const value = await fetcher();
    await this.set(key, value, ttl);
    return value;
  }

  async delete(key: string): Promise<void> {
    await redis.del(this.key(key));
  }

  async deletePattern(pattern: string): Promise<void> {
    const keys = await redis.keys(this.key(pattern));
    if (keys.length > 0) {
      await redis.del(...keys);
    }
  }

  // Cache with stale-while-revalidate
  async getStale<T>(
    key: string,
    fetcher: () => Promise<T>,
    options: { ttl: number; staleTTL: number }
  ): Promise<T> {
    const cacheKey = this.key(key);
    const staleKey = `${cacheKey}:stale`;

    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // Check stale data
    const stale = await redis.get(staleKey);
    if (stale) {
      // Return stale, refresh in background
      this.refreshCache(key, fetcher, options).catch(console.error);
      return JSON.parse(stale);
    }

    return this.refreshCache(key, fetcher, options);
  }

  private async refreshCache<T>(
    key: string,
    fetcher: () => Promise<T>,
    options: { ttl: number; staleTTL: number }
  ): Promise<T> {
    const value = await fetcher();
    const serialized = JSON.stringify(value);

    const pipeline = redis.pipeline();
    pipeline.setex(this.key(key), options.ttl, serialized);
    pipeline.setex(`${this.key(key)}:stale`, options.staleTTL, serialized);
    await pipeline.exec();

    return value;
  }
}

// Usage
const cache = new Cache({ prefix: 'user:', ttl: 3600 });

async function getUser(id: string) {
  return cache.getOrSet(`profile:${id}`, async () => {
    return await db.users.findById(id);
  }, 1800);
}
```

## Session Storage

```typescript
// patterns/session.ts
import { redis } from './client';
import { nanoid } from 'nanoid';

interface Session {
  id: string;
  userId: string;
  data: Record<string, any>;
  createdAt: number;
  expiresAt: number;
}

export class SessionStore {
  private prefix = 'session:';
  private userPrefix = 'user:sessions:';
  private ttl = 86400 * 7; // 7 days

  private key(sessionId: string): string {
    return `${this.prefix}${sessionId}`;
  }

  async create(userId: string, data: Record<string, any> = {}): Promise<Session> {
    const session: Session = {
      id: nanoid(32),
      userId,
      data,
      createdAt: Date.now(),
      expiresAt: Date.now() + this.ttl * 1000,
    };

    const pipeline = redis.pipeline();

    // Store session
    pipeline.setex(this.key(session.id), this.ttl, JSON.stringify(session));

    // Track user's sessions
    pipeline.sadd(`${this.userPrefix}${userId}`, session.id);
    pipeline.expire(`${this.userPrefix}${userId}`, this.ttl);

    await pipeline.exec();

    return session;
  }

  async get(sessionId: string): Promise<Session | null> {
    const data = await redis.get(this.key(sessionId));
    if (!data) return null;

    const session = JSON.parse(data) as Session;

    // Check expiration
    if (session.expiresAt < Date.now()) {
      await this.destroy(sessionId);
      return null;
    }

    return session;
  }

  async update(sessionId: string, data: Record<string, any>): Promise<void> {
    const session = await this.get(sessionId);
    if (!session) throw new Error('Session not found');

    session.data = { ...session.data, ...data };

    await redis.setex(
      this.key(sessionId),
      this.ttl,
      JSON.stringify(session)
    );
  }

  async refresh(sessionId: string): Promise<void> {
    const session = await this.get(sessionId);
    if (!session) return;

    session.expiresAt = Date.now() + this.ttl * 1000;

    await redis.setex(
      this.key(sessionId),
      this.ttl,
      JSON.stringify(session)
    );
  }

  async destroy(sessionId: string): Promise<void> {
    const session = await this.get(sessionId);
    if (!session) return;

    const pipeline = redis.pipeline();
    pipeline.del(this.key(sessionId));
    pipeline.srem(`${this.userPrefix}${session.userId}`, sessionId);
    await pipeline.exec();
  }

  async destroyAllForUser(userId: string): Promise<void> {
    const sessionIds = await redis.smembers(`${this.userPrefix}${userId}`);

    if (sessionIds.length > 0) {
      const keys = sessionIds.map(id => this.key(id));
      await redis.del(...keys, `${this.userPrefix}${userId}`);
    }
  }
}
```

## Rate Limiting

```typescript
// patterns/rate-limiter.ts
import { redis } from './client';

interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  resetAt: number;
}

export class RateLimiter {
  // Fixed window rate limiting
  async fixedWindow(
    key: string,
    limit: number,
    windowSeconds: number
  ): Promise<RateLimitResult> {
    const redisKey = `ratelimit:fixed:${key}`;
    const now = Math.floor(Date.now() / 1000);
    const window = Math.floor(now / windowSeconds);
    const windowKey = `${redisKey}:${window}`;

    const count = await redis.incr(windowKey);

    if (count === 1) {
      await redis.expire(windowKey, windowSeconds);
    }

    return {
      allowed: count <= limit,
      remaining: Math.max(0, limit - count),
      resetAt: (window + 1) * windowSeconds * 10

Related in Backend & APIs