Claude Code Plugins

Community-maintained marketplace

Feedback

Implement or debug Redis caching strategies using the centralized Upstash Redis client. Use when adding cache layers, debugging cache issues, or optimizing cache invalidation.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name redis-cache
description Implement or debug Redis caching strategies using the centralized Upstash Redis client. Use when adding cache layers, debugging cache issues, or optimizing cache invalidation.
allowed-tools Read, Edit, Grep, Glob

Redis Cache Management Skill

This skill helps you implement and optimize Redis caching in packages/utils/ and across the monorepo.

When to Use This Skill

  • Implementing caching for expensive database queries
  • Adding cache layers to API endpoints
  • Debugging cache hit/miss issues
  • Implementing cache invalidation strategies
  • Optimizing cache TTL (Time To Live)
  • Setting up cache warming
  • Managing cache keys and namespaces

Redis Architecture

The project uses Upstash Redis with a centralized client:

packages/utils/
├── src/
│   └── redis.ts          # Centralized Redis client
apps/api/
├── src/
│   └── lib/
│       └── cache/
│           ├── cars.ts   # Cars data caching
│           ├── coe.ts    # COE data caching
│           └── posts.ts  # Blog posts caching

Centralized Redis Client

// packages/utils/src/redis.ts
import { Redis } from "@upstash/redis";

if (!process.env.UPSTASH_REDIS_REST_URL) {
  throw new Error("UPSTASH_REDIS_REST_URL is not defined");
}

if (!process.env.UPSTASH_REDIS_REST_TOKEN) {
  throw new Error("UPSTASH_REDIS_REST_TOKEN is not defined");
}

export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL,
  token: process.env.UPSTASH_REDIS_REST_TOKEN,
});

Export from package:

// packages/utils/src/index.ts
export { redis } from "./redis";

Basic Cache Patterns

Simple Get/Set

import { redis } from "@sgcarstrends/utils";

// Set value
await redis.set("key", "value");

// Get value
const value = await redis.get("key");
console.log(value); // "value"

// Set with expiration (in seconds)
await redis.set("key", "value", { ex: 3600 }); // Expires in 1 hour

// Set if not exists
await redis.setnx("key", "value");

JSON Data Caching

import { redis } from "@sgcarstrends/utils";

// Cache object
const car = { make: "Toyota", model: "Camry", year: 2024 };
await redis.set("car:1", JSON.stringify(car));

// Retrieve object
const cached = await redis.get("car:1");
const parsedCar = JSON.parse(cached as string);

Cache with Type Safety

import { redis } from "@sgcarstrends/utils";

interface Car {
  make: string;
  model: string;
  year: number;
}

async function getCachedCar(id: string): Promise<Car | null> {
  const cached = await redis.get<string>(`car:${id}`);

  if (!cached) return null;

  return JSON.parse(cached) as Car;
}

async function setCachedCar(id: string, car: Car, ttl: number = 3600) {
  await redis.set(`car:${id}`, JSON.stringify(car), { ex: ttl });
}

Caching Strategies

Cache-Aside (Lazy Loading)

Most common pattern - check cache first, then database:

// apps/api/src/lib/cache/cars.ts
import { redis } from "@sgcarstrends/utils";
import { db } from "@sgcarstrends/database";
import { cars } from "@sgcarstrends/database/schema";
import { eq } from "drizzle-orm";

export async function getCarWithCache(id: string) {
  const cacheKey = `car:${id}`;

  // 1. Try to get from cache
  const cached = await redis.get<string>(cacheKey);

  if (cached) {
    console.log("Cache hit!");
    return JSON.parse(cached);
  }

  console.log("Cache miss!");

  // 2. If not in cache, get from database
  const car = await db.query.cars.findFirst({
    where: eq(cars.id, id),
  });

  if (!car) {
    return null;
  }

  // 3. Store in cache for next time
  await redis.set(cacheKey, JSON.stringify(car), {
    ex: 3600, // 1 hour TTL
  });

  return car;
}

Write-Through Cache

Update cache when writing to database:

import { redis } from "@sgcarstrends/utils";
import { db } from "@sgcarstrends/database";
import { cars } from "@sgcarstrends/database/schema";

export async function createCarWithCache(carData: NewCar) {
  // 1. Write to database
  const [car] = await db.insert(cars).values(carData).returning();

  // 2. Write to cache
  await redis.set(`car:${car.id}`, JSON.stringify(car), { ex: 3600 });

  // 3. Invalidate list caches
  await redis.del("cars:all");

  return car;
}

export async function updateCarWithCache(id: string, updates: Partial<Car>) {
  // 1. Update database
  const [car] = await db
    .update(cars)
    .set(updates)
    .where(eq(cars.id, id))
    .returning();

  // 2. Update cache
  await redis.set(`car:${id}`, JSON.stringify(car), { ex: 3600 });

  // 3. Invalidate related caches
  await redis.del("cars:all");
  await redis.del(`cars:make:${car.make}`);

  return car;
}

Cache Invalidation

import { redis } from "@sgcarstrends/utils";

// Delete single key
export async function invalidateCarCache(id: string) {
  await redis.del(`car:${id}`);
}

// Delete multiple keys
export async function invalidateCarCaches(ids: string[]) {
  const keys = ids.map(id => `car:${id}`);
  await redis.del(...keys);
}

// Delete by pattern (use sparingly - expensive operation)
export async function invalidateCarsByPattern(pattern: string) {
  const keys = await redis.keys(pattern);
  if (keys.length > 0) {
    await redis.del(...keys);
  }
}

// Example: Invalidate all car caches
await invalidateCarsByPattern("car:*");

Cache Key Strategies

Key Naming Conventions

// Good key naming patterns
const keys = {
  // Entity by ID
  car: (id: string) => `car:${id}`,
  coe: (id: string) => `coe:${id}`,

  // List/Collection
  allCars: () => "cars:all",
  carsByMake: (make: string) => `cars:make:${make}`,
  carsByMonth: (month: string) => `cars:month:${month}`,

  // Computed/Aggregated
  carStats: (month: string) => `stats:cars:${month}`,
  coeStats: (biddingNo: number) => `stats:coe:${biddingNo}`,

  // User-specific
  userPreferences: (userId: string) => `user:${userId}:preferences`,

  // Session
  session: (sessionId: string) => `session:${sessionId}`,
};

// Usage
await redis.set(keys.car("123"), JSON.stringify(carData));
await redis.get(keys.carsByMake("Toyota"));

Namespacing

const CACHE_PREFIX = "sgcarstrends";

function buildKey(...parts: string[]): string {
  return [CACHE_PREFIX, ...parts].join(":");
}

// Usage
const key = buildKey("cars", "make", "Toyota"); // "sgcarstrends:cars:make:Toyota"

TTL Strategies

Time-Based Expiration

// Different TTLs for different data types
const TTL = {
  SHORT: 60,          // 1 minute - rapidly changing data
  MEDIUM: 300,        // 5 minutes - moderately changing data
  LONG: 3600,         // 1 hour - slowly changing data
  DAY: 86400,         // 24 hours - daily data
  WEEK: 604800,       // 7 days - weekly data
  MONTH: 2592000,     // 30 days - monthly data
};

// Usage
await redis.set("realtime-data", data, { ex: TTL.SHORT });
await redis.set("daily-stats", stats, { ex: TTL.DAY });
await redis.set("monthly-report", report, { ex: TTL.MONTH });

Conditional Expiration

async function cacheWithSmartTTL(key: string, data: any) {
  const now = new Date();
  const hour = now.getHours();

  let ttl: number;

  // Short TTL during business hours (more frequent updates)
  if (hour >= 9 && hour <= 18) {
    ttl = 300; // 5 minutes
  } else {
    ttl = 3600; // 1 hour off-hours
  }

  await redis.set(key, JSON.stringify(data), { ex: ttl });
}

Advanced Patterns

Cache Stampede Prevention

Prevent multiple requests from hitting database simultaneously:

import { redis } from "@sgcarstrends/utils";

async function getWithStampedePrevention<T>(
  key: string,
  fetchFn: () => Promise<T>,
  ttl: number = 3600
): Promise<T> {
  // Try to get from cache
  const cached = await redis.get<string>(key);
  if (cached) {
    return JSON.parse(cached) as T;
  }

  // Use a lock to prevent stampede
  const lockKey = `${key}:lock`;
  const lockAcquired = await redis.setnx(lockKey, "1");

  if (lockAcquired) {
    // This request will fetch the data
    try {
      await redis.expire(lockKey, 10); // Lock expires in 10 seconds

      const data = await fetchFn();

      await redis.set(key, JSON.stringify(data), { ex: ttl });

      return data;
    } finally {
      await redis.del(lockKey);
    }
  } else {
    // Wait for the other request to finish
    await new Promise(resolve => setTimeout(resolve, 100));

    // Try again
    return getWithStampedePrevention(key, fetchFn, ttl);
  }
}

// Usage
const cars = await getWithStampedePrevention(
  "cars:all",
  () => db.query.cars.findMany(),
  3600
);

Stale-While-Revalidate

Serve stale data while refreshing in background:

async function getWithSWR<T>(
  key: string,
  fetchFn: () => Promise<T>,
  ttl: number = 3600,
  staleTime: number = 300
): Promise<T> {
  const cached = await redis.get<string>(key);

  if (cached) {
    const data = JSON.parse(cached) as T;

    // Check if data is stale
    const ttlRemaining = await redis.ttl(key);

    if (ttlRemaining < staleTime) {
      // Data is stale, refresh in background
      fetchFn().then(freshData => {
        redis.set(key, JSON.stringify(freshData), { ex: ttl });
      });
    }

    return data;
  }

  // No cache, fetch and cache
  const data = await fetchFn();
  await redis.set(key, JSON.stringify(data), { ex: ttl });

  return data;
}

Layered Caching

Combine memory cache with Redis:

import { LRUCache } from "lru-cache";
import { redis } from "@sgcarstrends/utils";

// In-memory L1 cache
const memoryCache = new LRUCache<string, any>({
  max: 500,
  ttl: 60000, // 1 minute
});

async function getWithLayeredCache<T>(
  key: string,
  fetchFn: () => Promise<T>
): Promise<T> {
  // 1. Check memory cache (L1)
  const memCached = memoryCache.get(key);
  if (memCached) {
    console.log("L1 cache hit");
    return memCached as T;
  }

  // 2. Check Redis cache (L2)
  const redisCached = await redis.get<string>(key);
  if (redisCached) {
    console.log("L2 cache hit");
    const data = JSON.parse(redisCached) as T;

    // Populate L1 cache
    memoryCache.set(key, data);

    return data;
  }

  // 3. Fetch from source
  console.log("Cache miss");
  const data = await fetchFn();

  // Populate both caches
  memoryCache.set(key, data);
  await redis.set(key, JSON.stringify(data), { ex: 3600 });

  return data;
}

Rate Limiting with Redis

import { Ratelimit } from "@upstash/ratelimit";
import { redis } from "@sgcarstrends/utils";

// Create rate limiter
const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, "10 s"), // 10 requests per 10 seconds
});

// Use in API route
export async function apiHandler(req: Request) {
  const ip = req.headers.get("x-forwarded-for") ?? "unknown";

  const { success, limit, reset, remaining } = await ratelimit.limit(ip);

  if (!success) {
    return new Response("Rate limit exceeded", {
      status: 429,
      headers: {
        "X-RateLimit-Limit": limit.toString(),
        "X-RateLimit-Remaining": remaining.toString(),
        "X-RateLimit-Reset": new Date(reset).toISOString(),
      },
    });
  }

  // Process request...
}

Cache Warming

Pre-populate cache with frequently accessed data:

import { redis } from "@sgcarstrends/utils";
import { db } from "@sgcarstrends/database";

export async function warmCarCache() {
  console.log("Warming car cache...");

  // Get frequently accessed makes
  const topMakes = ["Toyota", "Honda", "BMW", "Mercedes"];

  for (const make of topMakes) {
    const cars = await db.query.cars.findMany({
      where: eq(cars.make, make),
    });

    await redis.set(
      `cars:make:${make}`,
      JSON.stringify(cars),
      { ex: 3600 }
    );

    console.log(`Cached ${cars.length} cars for ${make}`);
  }

  console.log("Cache warming complete!");
}

// Run on application startup or scheduled job

Monitoring and Debugging

Cache Hit/Miss Tracking

let cacheHits = 0;
let cacheMisses = 0;

async function getWithMetrics<T>(
  key: string,
  fetchFn: () => Promise<T>
): Promise<T> {
  const cached = await redis.get<string>(key);

  if (cached) {
    cacheHits++;
    console.log(`Cache hit rate: ${(cacheHits / (cacheHits + cacheMisses) * 100).toFixed(2)}%`);
    return JSON.parse(cached) as T;
  }

  cacheMisses++;
  const data = await fetchFn();
  await redis.set(key, JSON.stringify(data), { ex: 3600 });

  return data;
}

Cache Size Monitoring

async function getCacheStats() {
  const info = await redis.info();
  const dbsize = await redis.dbsize();

  return {
    dbsize,
    info,
  };
}

Testing Cache Logic

// __tests__/cache/cars.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { redis } from "@sgcarstrends/utils";
import { getCarWithCache } from "../cache/cars";

// Mock Redis
vi.mock("@sgcarstrends/utils", () => ({
  redis: {
    get: vi.fn(),
    set: vi.fn(),
    del: vi.fn(),
  },
}));

describe("Car Cache", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("returns cached data when available", async () => {
    const cachedCar = { id: "1", make: "Toyota" };

    vi.mocked(redis.get).mockResolvedValue(JSON.stringify(cachedCar));

    const result = await getCarWithCache("1");

    expect(result).toEqual(cachedCar);
    expect(redis.get).toHaveBeenCalledWith("car:1");
  });

  it("fetches from database on cache miss", async () => {
    vi.mocked(redis.get).mockResolvedValue(null);

    const result = await getCarWithCache("1");

    expect(redis.get).toHaveBeenCalled();
    expect(redis.set).toHaveBeenCalled();
  });
});

Environment Variables

Required environment variables:

# Upstash Redis
UPSTASH_REDIS_REST_URL=https://your-instance.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token-here

Common Pitfalls

1. Caching Mutable Objects

// ❌ Bad - caching object reference
const data = { count: 1 };
await redis.set("key", data); // Won't work!

// ✅ Good - serialize to JSON
await redis.set("key", JSON.stringify(data));

2. Not Setting TTL

// ❌ Bad - data never expires
await redis.set("key", "value");

// ✅ Good - set appropriate TTL
await redis.set("key", "value", { ex: 3600 });

3. Cache Invalidation Bugs

// ❌ Bad - forgot to invalidate related caches
await db.update(cars).set({ make: "Honda" });

// ✅ Good - invalidate all related caches
await db.update(cars).set({ make: "Honda" });
await redis.del(`car:${id}`);
await redis.del("cars:all");
await redis.del(`cars:make:Toyota`);
await redis.del(`cars:make:Honda`);

References

  • Upstash Redis: Use Context7 for latest docs
  • Related files:
    • packages/utils/src/redis.ts - Redis client
    • apps/api/src/lib/cache/ - Cache implementations
    • Root CLAUDE.md - Project documentation

Best Practices

  1. Always Set TTL: Prevent unbounded cache growth
  2. Serialize Data: Use JSON.stringify/parse for objects
  3. Key Naming: Use consistent, descriptive key patterns
  4. Invalidation: Invalidate cache on writes
  5. Error Handling: Gracefully handle Redis failures
  6. Monitoring: Track cache hit/miss rates
  7. Testing: Test cache logic thoroughly
  8. Layered Caching: Consider L1 (memory) + L2 (Redis)