redis-cache
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 clientapps/api/src/lib/cache/- Cache implementations- Root CLAUDE.md - Project documentation
Best Practices
- Always Set TTL: Prevent unbounded cache growth
- Serialize Data: Use JSON.stringify/parse for objects
- Key Naming: Use consistent, descriptive key patterns
- Invalidation: Invalidate cache on writes
- Error Handling: Gracefully handle Redis failures
- Monitoring: Track cache hit/miss rates
- Testing: Test cache logic thoroughly
- Layered Caching: Consider L1 (memory) + L2 (Redis)
Quick Install
/plugin add https://github.com/sgcarstrends/sgcarstrends/tree/main/redis-cacheCopy and paste this command in Claude Code to install this skill
GitHub 仓库
Related Skills
subagent-driven-development
DevelopmentThis 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.
algorithmic-art
MetaThis 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.
executing-plans
DesignUse 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.
cost-optimization
OtherThis 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.
