Agent Skill
2/7/2026

redis-cache-patterns

Use this skill when the user asks about "Redis", "caching", "cache invalidation", "TTL", "circuit breaker", "fire-and-forget", "connection pooling", or any Redis caching work. Provides Redis caching patterns with resilience and cost optimization.

T
trantuananh
0GitHub Stars
1Views
npx skills add trantuananh-17/product-reviews

SKILL.md

Nameredis-cache-patterns
DescriptionUse this skill when the user asks about "Redis", "caching", "cache invalidation", "TTL", "circuit breaker", "fire-and-forget", "connection pooling", or any Redis caching work. Provides Redis caching patterns with resilience and cost optimization.

name: redis-cache-patterns description: Use this skill when the user asks about "Redis", "caching", "cache invalidation", "TTL", "circuit breaker", "fire-and-forget", "connection pooling", or any Redis caching work. Provides Redis caching patterns with resilience and cost optimization.

Redis Caching Best Practices

Overview

Redis caching reduces Firestore reads and improves response latency. However, Redis introduces concerns around max connections, network egress costs, and failure handling.

Key Principles:

  • Fail fast, fail safe - Never block requests due to Redis issues
  • Fire-and-forget writes - Cache writes should not slow down responses
  • Circuit breaker - Temporarily disable Redis on connection issues
  • Graceful degradation - Always fall back to Firestore

Connection Management

Singleton Pattern with Lazy Connection

let client = null;
let connectionPromise = null;

async function getRedisClient() {
  if (client?.isOpen) {
    return client;
  }

  if (connectionPromise) {
    return connectionPromise;
  }

  connectionPromise = (async () => {
    try {
      client = createClient({
        username: config.username,
        password: config.password,
        socket: {
          host: config.host,
          port: config.port,
          connectTimeout: 500, // Fast fail
          reconnectStrategy: false // Circuit breaker handles reconnection
        }
      });

      client.on('error', handleConnectionError);
      await client.connect();
      return client;
    } catch (e) {
      connectionPromise = null;
      client = null;
      return null;
    }
  })();

  return connectionPromise;
}

Circuit Breaker Pattern

Temporarily disable Redis on repeated failures:

let isDisabled = false;
let disabledUntil = 0;
const DISABLE_DURATION_MS = 60000; // 1 minute

function isRedisDisabled() {
  if (!isDisabled) return false;

  if (Date.now() > disabledUntil) {
    isDisabled = false;
    return false;
  }
  return true;
}

function disableRedis() {
  isDisabled = true;
  disabledUntil = Date.now() + DISABLE_DURATION_MS;
  console.log('Redis disabled for 60s due to connection issues');
}

Timeout Handling

OperationTimeoutWhy
Connection500msFail fast if Redis unreachable
Cache Read300msQuick fallback to Firestore
Cache WriteNoneFire-and-forget, non-blocking

Timeout Wrapper

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Redis timeout')), ms)
  );
  return Promise.race([promise, timeout]);
}

async function getCache(key) {
  try {
    const client = await getRedisClient();
    if (!client) return null;

    const value = await withTimeout(client.get(key), 300);
    return value ? JSON.parse(value) : null;
  } catch (e) {
    return null; // Fail silently, fall back to DB
  }
}

Fire-and-Forget Writes

Cache writes should never block the response:

// GOOD: Non-blocking write
function setCache(key, value) {
  getRedisClient()
    .then(client => {
      if (!client?.isOpen) return;
      return client.set(key, JSON.stringify(value));
    })
    .catch(() => {}); // Silently ignore failures
}

// BAD: Blocking write
async function setCache(key, value) {
  const client = await getRedisClient();
  await client.set(key, JSON.stringify(value)); // Blocks response!
}

Cache-Aside Pattern (Read-Through)

async function getEntityCached(entityId) {
  const cacheKey = `entity:${entityId}`;

  // 1. Try cache first (fast timeout)
  const cached = await getCache(cacheKey);
  if (cached) {
    return cached;
  }

  // 2. Cache miss - fetch from Firestore
  const entity = await getEntityFromFirestore(entityId);

  // 3. Cache for next time (fire-and-forget)
  if (entity) {
    setCache(cacheKey, entity);
  }

  return entity;
}

TTL Strategy

Data TypeTTLRationale
Configuration (shop settings)No expiryManual invalidation on update
Notification templates30 daysRarely changes
Session/token data1-24 hoursSecurity, auto-cleanup
Rate limit counters1 minuteAuto-reset
Temporary/computed data5-60 minutesBalance freshness vs cost

Cache Invalidation

On Update

async function updateEntity(entityId, updateData) {
  // 1. Update Firestore
  await firestoreUpdate(entityId, updateData);

  // 2. Invalidate cache (next read will re-cache)
  deleteCache(`entity:${entityId}`);
}

Bulk Invalidation

function invalidateMultiple(keys) {
  if (!keys?.length) return;

  getRedisClient()
    .then(client => {
      if (!client?.isOpen) return;
      return client.del(keys);
    })
    .catch(() => {});
}

Checklist

- Singleton connection with lazy initialization
- Circuit breaker for max connections / failures
- Connection timeout: 500ms
- Read timeout: 300ms
- Fire-and-forget writes (non-blocking)
- Graceful fallback to Firestore on any error
- Cache invalidation on entity updates
- TTL set for temporary data
- Key naming convention established
Skills Info
Original Name:redis-cache-patternsAuthor:trantuananh