Back to Skills

redis-cache

sgcarstrends
Updated Today
17 views
9
1
9
View on GitHub
Testinggeneral

About

This skill helps developers implement and debug Redis caching strategies using a centralized Upstash Redis client. Use it when adding cache layers to API endpoints, optimizing cache invalidation, or debugging cache hit/miss issues. It provides capabilities for managing cache TTL, setting up cache warming, and handling cache keys across the monorepo.

Documentation

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)

Quick Install

/plugin add https://github.com/sgcarstrends/sgcarstrends/tree/main/redis-cache

Copy and paste this command in Claude Code to install this skill

GitHub 仓库

sgcarstrends/sgcarstrends
Path: .claude/skills/redis-cache
apiaws-lambdabackendhonojob-schedulerneon-postgres

Related Skills

subagent-driven-development

Development

This skill executes implementation plans by dispatching a fresh subagent for each independent task, with code review between tasks. It enables fast iteration while maintaining quality gates through this review process. Use it when working on mostly independent tasks within the same session to ensure continuous progress with built-in quality checks.

View skill

algorithmic-art

Meta

This Claude Skill creates original algorithmic art using p5.js with seeded randomness and interactive parameters. It generates .md files for algorithmic philosophies, plus .html and .js files for interactive generative art implementations. Use it when developers need to create flow fields, particle systems, or other computational art while avoiding copyright issues.

View skill

executing-plans

Design

Use the executing-plans skill when you have a complete implementation plan to execute in controlled batches with review checkpoints. It loads and critically reviews the plan, then executes tasks in small batches (default 3 tasks) while reporting progress between each batch for architect review. This ensures systematic implementation with built-in quality control checkpoints.

View skill

cost-optimization

Other

This Claude Skill helps developers optimize cloud costs through resource rightsizing, tagging strategies, and spending analysis. It provides a framework for reducing cloud expenses and implementing cost governance across AWS, Azure, and GCP. Use it when you need to analyze infrastructure costs, right-size resources, or meet budget constraints.

View skill