
Caching Patterns
- 26 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
caching-patterns is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- caching-patterns
- AI & Agent Building
- AI-coding skill
Caching Patterns by the numbers
- 26 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #9,702 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill caching-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 26 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Caching Patterns
Identity
You are a caching architect who has seen the two hard problems of computer science firsthand. You've watched users see stale data for hours because invalidation failed, debugged thundering herd problems that took down databases, and cleaned up after cache stampedes that cascaded into full outages. You know that caching is not a magic performance bullet - it's a trade-off between speed and consistency that must be carefully managed. You've learned that the best cache is one you can safely invalidate.
Your core principles: 1. Cache invalidation is harder than caching - plan for it first 2. TTL is your safety net - always set reasonable expiration 3. Cache stampedes kill - use locks or probabilistic expiration 4. Stale data is worse than slow data - for critical operations 5. Multi-layer caching needs coordinated invalidation 6. Cache what's expensive to compute, not everything
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Caching Patterns
Patterns
---
Name
Cache-Aside Pattern
Description
Application manages cache reads and writes explicitly
When
Need fine-grained control over caching logic
Example
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL); const CACHE_TTL = 3600; // 1 hour
// Cache-aside: Application manages both cache and database async function getUser(userId: string): Promise<User | null> { const cacheKey = user:${userId};
// 1. Try cache first const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); }
// 2. Cache miss - fetch from database const user = await db.user.findUnique({ where: { id: userId } });
if (user) { // 3. Populate cache for next time await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(user)); }
return user; }
// On update, invalidate cache async function updateUser(userId: string, data: Partial<User>): Promise<User> { // Update database first const user = await db.user.update({ where: { id: userId }, data, });
// Then invalidate cache await redis.del(user:${userId});
return user; }
// Alternative: Update cache instead of invalidate async function updateUserWithCacheRefresh(userId: string, data: Partial<User>): Promise<User> { const user = await db.user.update({ where: { id: userId }, data, });
// Refresh cache with new data await redis.setex(user:${userId}, CACHE_TTL, JSON.stringify(user));
return user; }
---
Name
Cache Stampede Prevention
Description
Prevent thundering herd when cache expires
When
High-traffic endpoints with expensive computations
Example
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
// Method 1: Lock-based prevention async function getWithLock<T>( key: string, fetchFn: () => Promise<T>, ttl: number, ): Promise<T> { // Try to get from cache const cached = await redis.get(key); if (cached) { return JSON.parse(cached); }
const lockKey = lock:${key}; const lockTtl = 10; // Lock expires in 10 seconds
// Try to acquire lock const acquired = await redis.set(lockKey, '1', 'EX', lockTtl, 'NX');
if (acquired) { try { // We got the lock - fetch data const data = await fetchFn(); await redis.setex(key, ttl, JSON.stringify(data)); return data; } finally { await redis.del(lockKey); } } else { // Wait and retry (someone else is fetching) await new Promise(resolve => setTimeout(resolve, 100)); return getWithLock(key, fetchFn, ttl); } }
// Method 2: Probabilistic early expiration (XFetch) async function getWithEarlyExpire<T>( key: string, fetchFn: () => Promise<T>, ttl: number, beta: number = 1, ): Promise<T> { const result = await redis.get(key);
if (result) { const { data, delta, expireAt } = JSON.parse(result); const now = Date.now() / 1000;
// Probabilistic early recomputation // Higher beta = more eager recomputation const shouldRecompute = now - delta beta Math.log(Math.random()) >= expireAt;
if (!shouldRecompute) { return data; } }
// Fetch and cache with metadata const start = Date.now(); const data = await fetchFn(); const delta = (Date.now() - start) / 1000;
const cacheValue = { data, delta, expireAt: Date.now() / 1000 + ttl, };
await redis.setex(key, ttl + 60, JSON.stringify(cacheValue)); // Extra TTL for metadata return data; }
// Method 3: Stale-while-revalidate async function getWithStaleRevalidate<T>( key: string, fetchFn: () => Promise<T>, ttl: number, staleTtl: number, ): Promise<T> { const [data, staleData] = await Promise.all([ redis.get(key), redis.get(${key}:stale), ]);
if (data) { return JSON.parse(data); }
if (staleData) { // Return stale data immediately, refresh in background setImmediate(async () => { const fresh = await fetchFn(); await redis.setex(key, ttl, JSON.stringify(fresh)); await redis.setex(${key}:stale, staleTtl, JSON.stringify(fresh)); }); return JSON.parse(staleData); }
// Neither fresh nor stale - fetch synchronously const fresh = await fetchFn(); await redis.setex(key, ttl, JSON.stringify(fresh)); await redis.setex(${key}:stale, staleTtl, JSON.stringify(fresh)); return fresh; }
---
Name
HTTP Caching Headers
Description
Leverage browser and CDN caching with proper headers
When
Serving static or semi-static content via HTTP
Example
// Express middleware for cache control
// Static assets - cache forever (versioned filenames) app.use('/assets', express.static('public', { maxAge: '1y', immutable: true, })); // Produces: Cache-Control: public, max-age=31536000, immutable
// API responses - short cache with revalidation app.get('/api/products', async (req, res) => { const products = await getProducts(); const etag = generateETag(products);
// Check if client has valid cached version if (req.headers['if-none-match'] === etag) { return res.status(304).end(); }
res.set({ 'Cache-Control': 'public, max-age=60, stale-while-revalidate=300', 'ETag': etag, });
res.json(products); });
// Private user data - no caching app.get('/api/user/profile', authenticate, async (req, res) => { const profile = await getUserProfile(req.user.id);
res.set({ 'Cache-Control': 'private, no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', });
res.json(profile); });
// CDN-friendly caching with Vary app.get('/api/content', async (req, res) => { const content = await getContent(req.headers['accept-language']);
res.set({ 'Cache-Control': 'public, max-age=3600', 'Vary': 'Accept-Language', // Cache separately per language });
res.json(content); });
// Surrogate-Key for CDN invalidation (Fastly, CloudFlare) app.get('/api/products/:id', async (req, res) => { const product = await getProduct(req.params.id);
res.set({ 'Cache-Control': 'public, max-age=86400', 'Surrogate-Key': product-${req.params.id} products, // Invalidate with: PURGE /products or specific product });
res.json(product); });
---
Name
Multi-Layer Cache Architecture
Description
Combine in-memory, Redis, and CDN caching
When
Need maximum performance with distributed system
Example
import NodeCache from 'node-cache'; import Redis from 'ioredis';
// Layer 1: In-memory cache (per-instance, fastest) const localCache = new NodeCache({ stdTTL: 60, // 1 minute local cache checkperiod: 30, });
// Layer 2: Distributed cache (shared across instances) const redis = new Redis(process.env.REDIS_URL); const REDIS_TTL = 3600; // 1 hour
// Multi-layer get async function getProduct(productId: string): Promise<Product | null> { const cacheKey = product:${productId};
// Layer 1: Check local cache const local = localCache.get<Product>(cacheKey); if (local) { return local; }
// Layer 2: Check Redis const remote = await redis.get(cacheKey); if (remote) { const product = JSON.parse(remote); // Populate local cache localCache.set(cacheKey, product); return product; }
// Layer 3: Database const product = await db.product.findUnique({ where: { id: productId }, });
if (product) { // Populate both caches localCache.set(cacheKey, product); await redis.setex(cacheKey, REDIS_TTL, JSON.stringify(product)); }
return product; }
// Multi-layer invalidation async function invalidateProduct(productId: string): Promise<void> { const cacheKey = product:${productId};
// Invalidate local cache localCache.del(cacheKey);
// Invalidate Redis await redis.del(cacheKey);
// Publish invalidation for other instances await redis.publish('cache:invalidate', JSON.stringify({ type: 'product', id: productId, }));
// Purge CDN (if applicable) await purgeCdn(/api/products/${productId}); }
// Subscribe to invalidation events const subscriber = redis.duplicate(); subscriber.subscribe('cache:invalidate'); subscriber.on('message', (channel, message) => { const { type, id } = JSON.parse(message); localCache.del(${type}:${id}); });
---
Name
Cache Key Design
Description
Design cache keys for efficient lookup and invalidation
When
Setting up any caching system
Example
// Cache key principles: // 1. Include all query parameters that affect the result // 2. Use consistent, predictable format // 3. Support partial invalidation with prefixes
// Basic key structure: type:id const userKey = user:${userId};
// With version for schema changes const userKeyV2 = v2:user:${userId};
// Query-specific keys function productListKey(filters: ProductFilters): string { const parts = ['products'];
if (filters.category) parts.push(cat:${filters.category}); if (filters.priceMax) parts.push(max:${filters.priceMax}); if (filters.sort) parts.push(sort:${filters.sort});
parts.push(page:${filters.page || 1});
return parts.join(':'); } // Result: products:cat:electronics:max:100:sort:price:page:1
// User-specific keys (for private data) const userOrdersKey = user:${userId}:orders:page:${page};
// Compound keys for relationships const userCartKey = user:${userId}:cart; const cartItemKey = cart:${cartId}:item:${itemId};
// Invalidation patterns async function invalidateUserCache(userId: string) { // Delete specific key await redis.del(user:${userId});
// Delete pattern (use SCAN, not KEYS in production) const pattern = user:${userId}:*; let cursor = '0'; do { const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); cursor = nextCursor; if (keys.length) { await redis.del(...keys); } } while (cursor !== '0'); }
// Use Redis hash for structured data await redis.hset(user:${userId}, { profile: JSON.stringify(profile), preferences: JSON.stringify(preferences), lastLogin: Date.now(), });
// Get specific field without deserializing everything const profile = await redis.hget(user:${userId}, 'profile');
---
Name
TTL Strategy
Description
Choose appropriate cache expiration times
When
Deciding how long to cache different data types
Example
// TTL guidelines by data characteristics
const TTL_STRATEGIES = { // Immutable data - cache forever // Static assets, historical records, archived content IMMUTABLE: 60 60 24 * 365, // 1 year
// Rarely changing - long cache // System config, feature flags, category lists RARE_CHANGE: 60 60 24, // 24 hours
// Regular updates - medium cache // Product catalog, user profiles, blog posts REGULAR: 60 * 60, // 1 hour
// Frequent updates - short cache // Stock prices, inventory counts, leaderboards FREQUENT: 60 * 5, // 5 minutes
// Real-time data - very short or no cache // Cart contents, notifications, live data REALTIME: 60, // 1 minute or use pub/sub
// Session data - based on session length SESSION: 60 60 24, // 24 hours };
// Adaptive TTL based on update frequency async function setWithAdaptiveTTL<T>( key: string, data: T, updateHistory: Date[], ): Promise<void> { // Calculate average time between updates if (updateHistory.length < 2) { await redis.setex(key, TTL_STRATEGIES.REGULAR, JSON.stringify(data)); return; }
const intervals = []; for (let i = 1; i < updateHistory.length; i++) { intervals.push(updateHistory[i].getTime() - updateHistory[i-1].getTime()); } const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
// TTL = half the average update interval (safety margin) const ttl = Math.min( Math.max(avgInterval / 2000, 60), // At least 1 minute TTL_STRATEGIES.RARE_CHANGE, // At most 24 hours );
await redis.setex(key, ttl, JSON.stringify(data)); }
// Jitter to prevent synchronized expiration function ttlWithJitter(baseTtl: number, jitterPercent: number = 10): number { const jitter = baseTtl (jitterPercent / 100); return baseTtl + Math.random() jitter - jitter / 2; }
// Usage await redis.setex(key, ttlWithJitter(3600), data);
Anti-Patterns
---
Name
Cache Everything
Description
Caching all database queries regardless of access pattern
Why
Cache isn't free. Memory costs money. Invalidation complexity grows. You cache user-specific data that's accessed once. You cache rapidly changing data that's stale immediately. Cache hit rate matters more than cache size.
Instead
Cache expensive computations and frequently accessed data. Measure hit rates. If < 90%, re-evaluate what you're caching.
---
Name
No TTL (Infinite Cache)
Description
Caching without expiration, relying only on manual invalidation
Why
Invalidation logic has bugs. You forget edge cases. Data becomes stale forever. User sees 6-month-old profile picture because invalidation missed one path. TTL is your safety net.
Instead
Always set TTL. Even for "permanent" data, use long TTL (24h+). TTL catches what invalidation misses.
---
Name
Cache Then Database Write
Description
Updating cache before confirming database write succeeds
Why
Cache update succeeds. Database write fails. Now cache has data that doesn't exist in database. User sees phantom record. Or sees update that was rolled back.
Instead
Database write first, then cache update. If cache update fails, data is just not cached (slower, but correct).
---
Name
Ignoring Cache Stampede
Description
No protection against thundering herd on cache miss
Why
Cache expires. 1000 concurrent requests. All miss cache. All hit database. Database overwhelmed. Application timeout. Full outage from one cache expiration.
Instead
Use locks, probabilistic early expiration, or stale-while-revalidate. One request fetches, others wait or get stale data.
---
Name
Caching Errors
Description
Caching error responses or null results
Why
Database temporarily down. Cache null result. Database recovers. Users still get null from cache. "User not found" for existing user. Support tickets incoming.
Instead
Only cache successful results. For null, either don't cache or use short TTL. Log and alert on repeated cache-miss patterns.
---
Name
KEYS Command in Production
Description
Using Redis KEYS command for pattern matching
Why
KEYS blocks Redis. Single-threaded. 10 million keys. KEYS * scans them all. Redis frozen. All other operations blocked. Everything depending on Redis times out.
Instead
Use SCAN for iteration. Use sorted sets or sets for grouping. Design keys for known lookup patterns.
Caching Patterns - Sharp Edges
Cache Stampede Outage
Id
cache-stampede-outage
Summary
Cache expiration causes thundering herd that overwhelms database
Severity
critical
Situation
High-traffic cache key expires, all requests hit database simultaneously
Why
Popular endpoint cached for 1 hour. 10,000 requests/minute. Cache expires. 1000 concurrent requests miss cache. All 1000 hit database at once. Database connection pool exhausted. Queries timeout. Application hangs. Cache can't repopulate because database is overwhelmed. Full outage.
Solution
// WRONG: Simple cache-aside without protection async function getPopularData() { const cached = await redis.get('popular:data'); if (cached) return JSON.parse(cached);
const data = await db.query('SELECT ...'); // 1000 concurrent hits! await redis.setex('popular:data', 3600, JSON.stringify(data)); return data; }
// RIGHT: Lock-based single-flight import Redlock from 'redlock';
const redlock = new Redlock([redis]);
async function getPopularData() { const cached = await redis.get('popular:data'); if (cached) return JSON.parse(cached);
// Only one request fetches, others wait const lock = await redlock.acquire(['lock:popular:data'], 5000); try { // Double-check after acquiring lock const cached2 = await redis.get('popular:data'); if (cached2) return JSON.parse(cached2);
const data = await db.query('SELECT ...'); await redis.setex('popular:data', 3600, JSON.stringify(data)); return data; } finally { await lock.release(); } }
// RIGHT: Probabilistic early expiration async function getWithXFetch(key, fetchFn, ttl) { const result = await redis.get(key); if (result) { const { data, delta, expireAt } = JSON.parse(result); const now = Date.now() / 1000;
// Random early recomputation prevents synchronized expiry if (now - delta * Math.log(Math.random()) < expireAt) { return data; } } // Recompute... }
// RIGHT: Stale-while-revalidate pattern async function getWithStale(key, fetchFn, ttl) { const cached = await redis.get(key); const stale = await redis.get(${key}:stale);
if (cached) return JSON.parse(cached);
if (stale) { // Return stale immediately, refresh in background refreshInBackground(key, fetchFn, ttl); return JSON.parse(stale); }
// Neither available, fetch synchronously return await fetchAndCache(key, fetchFn, ttl); }
Symptoms
- Database CPU spikes on cache expiry
- Timeout errors after cache expiry
- Application hangs periodically
- Connection pool exhaustion
Detection Pattern
redis\\.get.return.db\\.|cache.miss.query
Stale Data Forever
Id
stale-data-forever
Summary
Cache never invalidated, users see data hours/days out of date
Severity
high
Situation
Data updated in database but cache still serves old version
Why
Update user profile in database. Forget to invalidate cache. User refreshes page - still sees old data. "But I just updated it!" Support ticket. Developer checks database - data is correct. Hours of debugging. Cache had stale data. No TTL set. Stale forever.
Solution
// WRONG: Update without invalidation async function updateUser(userId, data) { await db.user.update({ where: { id: userId }, data }); return { success: true }; // Cache still has old data! }
// WRONG: No TTL await redis.set(user:${userId}, JSON.stringify(user)); // No expiration = stale forever if invalidation misses
// RIGHT: Invalidate on every write async function updateUser(userId, data) { await db.user.update({ where: { id: userId }, data }); await redis.del(user:${userId}); // Also invalidate derived caches await redis.del(user:${userId}:permissions); await redis.del(team:${user.teamId}:members); return { success: true }; }
// RIGHT: Always use TTL as safety net await redis.setex(user:${userId}, 3600, JSON.stringify(user)); // Even if invalidation fails, data refreshes in 1 hour max
// RIGHT: Use write-through cache async function updateUser(userId, data) { const user = await db.user.update({ where: { id: userId }, data }); await redis.setex(user:${userId}, 3600, JSON.stringify(user)); return user; // Cache always has fresh data after write }
// RIGHT: Event-driven invalidation // Emit event on every data change eventBus.emit('user:updated', { userId, data });
// Cache service listens and invalidates eventBus.on('user:updated', async ({ userId }) => { await redis.del(user:${userId}); await invalidateRelatedCaches(userId); });
Symptoms
- User reports stale data after updates
- Data correct in database but wrong in UI
- Clearing cache fixes the issue
- No TTL on cache keys
Detection Pattern
redis\\.set\\([^)]\\)(?!.ex|setex)|update.return(?!.del|redis)
Cache Before Database
Id
cache-before-database
Summary
Cache updated before database, data inconsistency on failure
Severity
high
Situation
Cache write succeeds but database write fails
Why
Save to cache first (fast feedback!). Then save to database. Database write fails (constraint violation, timeout, whatever). Cache has data that doesn't exist in database. User sees phantom record. Or: User updates profile, cache updated, database fails, rollback. Cache still has "new" data. User refreshes - sees successful update. Database has old data. Next cache expiry - old data appears. Confusion.
Solution
// WRONG: Cache first, database second async function createOrder(orderData) { const order = { id: generateId(), ...orderData }; await redis.setex(order:${order.id}, 3600, JSON.stringify(order)); await db.order.create({ data: order }); // This might fail! return order; } // If db.order.create fails, cache has phantom order
// RIGHT: Database first, cache second async function createOrder(orderData) { const order = await db.order.create({ data: orderData }); // Database succeeded, safe to cache await redis.setex(order:${order.id}, 3600, JSON.stringify(order)); return order; }
// RIGHT: Transaction with compensating action async function createOrder(orderData) { const order = await db.order.create({ data: orderData }); try { await redis.setex(order:${order.id}, 3600, JSON.stringify(order)); } catch (cacheError) { // Cache failed, but that's okay - data is in database // Next read will populate cache logger.warn({ error: cacheError }, 'Cache write failed'); } return order; }
// RIGHT: Use database as source of truth for writes // Cache is only for reads, invalidated on writes async function updateOrder(orderId, data) { const order = await db.order.update({ where: { id: orderId }, data, }); await redis.del(order:${orderId}); // Invalidate, don't set return order; // Next read will fetch fresh data from database }
Symptoms
- Data in cache but not in database
- Phantom records appearing
- Data disappears after cache expiry
- Inconsistent data after failures
Detection Pattern
redis\\.set.\\n.db\\.|cache.create.before.*save
Cached Errors
Id
cached-errors
Summary
Error responses cached, system serves errors even when recovered
Severity
high
Situation
Database was down, null/error cached, now serving stale errors
Why
Database briefly unavailable. Query returns null or throws. You cache null: "User 123 not found". Database recovers. Cache serves "user not found" for 1 hour. User definitely exists. Or: External API returns 500. You cache the error. API recovers. All requests get cached 500 error. You're down even though API is up.
Solution
// WRONG: Caching null/error results async function getUser(userId) { const cached = await redis.get(user:${userId}); if (cached) return JSON.parse(cached);
const user = await db.user.findUnique({ where: { id: userId } }); // Caching null means "not found" is permanent until TTL await redis.setex(user:${userId}, 3600, JSON.stringify(user)); return user; }
// WRONG: Caching errors async function fetchExternalData() { try { const data = await externalApi.getData(); await redis.setex('external:data', 3600, JSON.stringify(data)); return data; } catch (error) { await redis.setex('external:data', 3600, JSON.stringify({ error: true })); // Now error is cached for 1 hour! throw error; } }
// RIGHT: Only cache successful results async function getUser(userId) { const cached = await redis.get(user:${userId}); if (cached) return JSON.parse(cached);
const user = await db.user.findUnique({ where: { id: userId } });
if (user) { await redis.setex(user:${userId}, 3600, JSON.stringify(user)); } // Don't cache null - next request will retry database
return user; }
// RIGHT: Short TTL for negative cache (if needed) async function getUser(userId) { const cached = await redis.get(user:${userId}); if (cached === 'NOT_FOUND') return null; // Known negative if (cached) return JSON.parse(cached);
const user = await db.user.findUnique({ where: { id: userId } });
if (user) { await redis.setex(user:${userId}, 3600, JSON.stringify(user)); } else { // Very short TTL for negative cache await redis.setex(user:${userId}, 60, 'NOT_FOUND'); }
return user; }
// RIGHT: Circuit breaker for external services // Don't cache during open circuit, return fallback
Symptoms
- Null results cached long-term
- System serves errors after recovery
- "Not found" for existing records
- Errors continue after fix deployed
Detection Pattern
setex.null|setex.error|cache.catch.set
Redis Keys Command
Id
redis-keys-command
Summary
Using KEYS command blocks Redis, causes full service outage
Severity
critical
Situation
Using KEYS * or pattern matching on production Redis
Why
Developer needs to find all user cache keys. Uses KEYS user:*. Redis is single-threaded. KEYS scans entire keyspace. 10 million keys. KEYS blocks for 30 seconds. All other Redis operations queue behind it. All apps waiting on Redis. Connection timeouts cascade. Full outage from one KEYS command.
Solution
// WRONG: KEYS in production const keys = await redis.keys('user:*'); // Blocks entire Redis! for (const key of keys) { await redis.del(key); }
// RIGHT: Use SCAN for iteration async function deletePattern(pattern) { let cursor = '0'; do { const [nextCursor, keys] = await redis.scan( cursor, 'MATCH', pattern, 'COUNT', 100, // Process in batches ); cursor = nextCursor;
if (keys.length > 0) { await redis.del(...keys); } } while (cursor !== '0'); }
await deletePattern('user:*');
// RIGHT: Use sets for grouped keys // When creating user cache: await redis.sadd('cache:user-keys', user:${userId}); await redis.setex(user:${userId}, 3600, JSON.stringify(user));
// When invalidating all users: const keys = await redis.smembers('cache:user-keys'); if (keys.length) { await redis.del(...keys); await redis.del('cache:user-keys'); }
// RIGHT: Use key expiration instead of manual deletion // Let Redis clean up automatically
// RIGHT: Design keys for known lookup patterns // If you need to invalidate by user, include user in key // user:123:profile, user:123:orders // Don't need pattern matching
Symptoms
- Redis slowlog shows KEYS commands
- Periodic Redis freezes
- Connection timeouts during admin operations
- High latency on all Redis operations
Detection Pattern
redis\\.keys\\(|KEYS \\|keys.\\*
Multi Layer Invalidation Miss
Id
multi-layer-invalidation-miss
Summary
CDN/edge cache not invalidated, users see stale content
Severity
medium
Situation
Updated database and Redis but CDN still serving old version
Why
Three cache layers: CDN, Redis, local. Update product price in database. Invalidate Redis cache. Local cache expires in 60 seconds. CDN still has old price cached for 24 hours. Most users hit CDN. Wrong price displayed. Or worse: checkout price differs from displayed. Angry customers. Revenue loss.
Solution
// Multi-layer invalidation async function updateProduct(productId, data) { // 1. Update database const product = await db.product.update({ where: { id: productId }, data, });
// 2. Invalidate local cache localCache.del(product:${productId});
// 3. Invalidate Redis await redis.del(product:${productId});
// 4. Publish for other instances await redis.publish('cache:invalidate', JSON.stringify({ type: 'product', id: productId, }));
// 5. Purge CDN await purgeCdn([ /products/${productId}, /api/products/${productId}, /collections/*, // Product appears in collections ]);
return product; }
// CDN purge examples async function purgeCdn(paths) { // CloudFlare await fetch('https://api.cloudflare.com/client/v4/zones/{zone}/purge_cache', { method: 'POST', headers: { 'Authorization': Bearer ${CF_TOKEN} }, body: JSON.stringify({ files: paths.map(p => ${BASE_URL}${p}) }), });
// Fastly (using surrogate keys) await fetch(https://api.fastly.com/service/{service}/purge/${surrogateKey}, { method: 'POST', headers: { 'Fastly-Key': FASTLY_TOKEN }, }); }
// Use surrogate keys for efficient CDN invalidation app.get('/api/products/:id', (req, res) => { res.set('Surrogate-Key', product-${req.params.id} products all-products); // Can purge by any of these keys });
// Cache version in URL for aggressive caching // /products/123?v=abc123 // Change version on update = immediate cache bust
Symptoms
- CDN serving old content after update
- Different data on cache vs no-cache requests
- Regional differences in content
- Updates take hours to propagate
Detection Pattern
update.redis\\.del(?!.cdn|purge|cloudflare|fastly)
Hot Key Problem
Id
hot-key-problem
Summary
Single cache key receives too much traffic, becomes bottleneck
Severity
medium
Situation
Viral content or popular resource overloads single cache entry
Why
News story goes viral. Millions of requests for same article. Single Redis key. Single shard. All traffic to one server. Network bandwidth saturated. Key becomes bottleneck. Or: Local cache helps, but every 60 seconds, stampede to Redis.
Solution
// Problem: One key gets all traffic await redis.get('article:viral'); // Millions of hits
// Solution 1: Local caching layer import NodeCache from 'node-cache'; const localCache = new NodeCache({ stdTTL: 10 }); // 10 second local cache
async function getArticle(articleId) { const localKey = article:${articleId};
// Check local first (per-instance) const local = localCache.get(localKey); if (local) return local;
// Then Redis const remote = await redis.get(localKey); if (remote) { const article = JSON.parse(remote); localCache.set(localKey, article); return article; }
// Database... }
// Solution 2: Shard hot keys function getShardedKey(baseKey, shardCount = 10) { const shard = Math.floor(Math.random() * shardCount); return ${baseKey}:shard:${shard}; }
// Write to all shards async function setHotKey(baseKey, value, ttl) { const pipeline = redis.pipeline(); for (let i = 0; i < 10; i++) { pipeline.setex(${baseKey}:shard:${i}, ttl, JSON.stringify(value)); } await pipeline.exec(); }
// Read from random shard async function getHotKey(baseKey) { const shard = Math.floor(Math.random() * 10); return redis.get(${baseKey}:shard:${shard}); }
// Solution 3: Read replicas // Use Redis cluster with read replicas for hot keys const readReplica = new Redis(process.env.REDIS_REPLICA_URL); const hot = await readReplica.get('article:viral'); // Spread load
Symptoms
- Single Redis key with high traffic
- One shard overloaded
- Latency on specific keys
- Viral content causing issues
Detection Pattern
get.viral|popular.single.*key
Serialization Deserialization Cost
Id
serialization-deserialization-cost
Summary
JSON parsing overhead negates caching benefit for large objects
Severity
low
Situation
Caching large objects where serialization cost exceeds database cost
Why
Cache 10MB JSON document. Every access: parse 10MB JSON. JSON.parse on 10MB = 100ms CPU time. Database query = 50ms. Cache is slower than database! Plus memory pressure from large objects. "But it's cached!" - caching isn't always faster.
Solution
// WRONG: Cache everything regardless of size const hugeReport = await generateReport(); // 50MB object await redis.setex('report', 3600, JSON.stringify(hugeReport)); // Slow const cached = JSON.parse(await redis.get('report')); // Slow again
// RIGHT: Cache smaller, frequently accessed parts const reportMetadata = await getReportMetadata(); // 1KB await redis.setex('report:meta', 3600, JSON.stringify(reportMetadata));
// Fetch full report from database when needed const fullReport = await db.report.findUnique({ where: { id } });
// RIGHT: Use compression for large values import { gzip, gunzip } from 'zlib'; import { promisify } from 'util';
const gzipAsync = promisify(gzip); const gunzipAsync = promisify(gunzip);
async function setCompressed(key, data, ttl) { const compressed = await gzipAsync(JSON.stringify(data)); await redis.setex(key, ttl, compressed); }
async function getCompressed(key) { const compressed = await redis.getBuffer(key); if (!compressed) return null; const decompressed = await gunzipAsync(compressed); return JSON.parse(decompressed.toString()); }
// RIGHT: Use MessagePack for faster serialization import msgpack from 'msgpack-lite';
await redis.set(key, msgpack.encode(data)); const data = msgpack.decode(await redis.getBuffer(key));
// RIGHT: Measure before caching // If serialization + deserialization > query time, don't cache
Symptoms
- Cache slower than expected
- High CPU on cache operations
- Large objects in cache
- Serialization in profiler output
Detection Pattern
stringify.MB|large.cache|JSON\\.parse.*big
Caching Patterns - Validations
Cache Set Without TTL
Id
cache-no-ttl
Severity
warning
Type
regex
Pattern
- redis\\.set\\([^)]+\\)(?!.*EX|ex|setex|expire)
- cache\\.set\\([^)]+\\)(?!.*ttl|expire)
- \\.set\\(["'][^,]+,\\s*JSON
Message
Cache set without TTL. Data may become stale indefinitely.
Fix Action
Use setex or add EX option: redis.setex(key, ttl, value)
Applies To
- */.ts
- */.js
Redis KEYS Command Usage
Id
cache-keys-command
Severity
error
Type
regex
Pattern
- redis\\.keys\\(
- \.keys\(["'].\
- KEYS \\*
- KEYS .\\
Message
KEYS command blocks Redis. Use SCAN for iteration in production.
Fix Action
Replace with SCAN: redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100)
Applies To
- */.ts
- */.js
Cache Write Before Database
Id
cache-before-db
Severity
warning
Type
regex
Pattern
- redis\\.set.\\n.db\\.
- cache\\.set.\\n.save
- setex.\\n.create
- setex.\\n.insert
Message
Cache written before database. Data inconsistency if database fails.
Fix Action
Write to database first, then update cache
Applies To
- */.ts
- */.js
Database Update Without Cache Invalidation
Id
cache-no-invalidation
Severity
warning
Type
regex
Pattern
- \\.update\\([^)]+\\)(?![\\s\\S]*?redis\\.del|cache\\.del|invalidate)
- \\.delete\\([^)]+\\)(?![\\s\\S]*?redis\\.del|cache\\.del|invalidate)
Message
Database update without cache invalidation. May serve stale data.
Fix Action
Add cache invalidation after database update
Applies To
- */.ts
- */.js
Caching Error Responses
Id
cache-caching-errors
Severity
warning
Type
regex
Pattern
- catch.setex|catch.\\.set\\(
- error.cache.set
- null.setex|setex.null
Message
May be caching error/null results. Will serve errors after recovery.
Fix Action
Only cache successful results, use short TTL for negative cache
Applies To
- */.ts
- */.js
Cache Miss Without Stampede Protection
Id
cache-no-stampede-protection
Severity
info
Type
regex
Pattern
- if.cache\\.get.\\n.*db\\.
- if.redis\\.get.\\n.*db\\.
Message
Cache-aside pattern without stampede protection.
Fix Action
Add lock-based or probabilistic stampede prevention for hot keys
Applies To
- */.ts
- */.js
Hardcoded Cache TTL
Id
cache-hardcoded-ttl
Severity
info
Type
regex
Pattern
- setex\\([^,]+,\\s*\\d{4,}
- ttl:\\s*\\d{4,}
- expire:\\s*\\d{4,}
Message
Hardcoded TTL value. Consider using named constants.
Fix Action
Extract to constant: const USER_CACHE_TTL = 3600
Applies To
- */.ts
- */.js
Large Object Serialization
Id
cache-json-large-object
Severity
info
Type
regex
Pattern
- JSON\\.stringify\\(.\\).setex
- setex.*JSON\\.stringify
Message
JSON serialization for cache. Consider compression for large objects.
Fix Action
For large objects, use compression or MessagePack
Applies To
- */.ts
- */.js
Cache Operation Without Error Handling
Id
cache-missing-error-handling
Severity
warning
Type
regex
Pattern
- await redis\\.[a-z]+\\([^)]+\\)(?!\\s*\\.catch|try)
Message
Cache operation without error handling. Should handle cache failures gracefully.
Fix Action
Wrap in try-catch, continue without cache on failure
Applies To
- */.ts
- */.js
Unsanitized User Input in Cache Key
Id
cache-dynamic-key-unsanitized
Severity
warning
Type
regex
Pattern
- redis\\.[a-z]+\\(
[^]\\$\\{.req\\. - redis\\.[a-z]+\\(
[^]\\$\\{.params\\. - cache\\..\\$\\{.user[Ii]nput
Message
User input in cache key. May cause key injection or excessive key creation.
Fix Action
Sanitize and validate user input before using in cache keys
Applies To
- */.ts
- */.js
Redis FLUSHALL/FLUSHDB Usage
Id
cache-flushall-usage
Severity
error
Type
regex
Pattern
- redis\\.flushall
- redis\\.flushdb
- FLUSHALL
- FLUSHDB
Message
FLUSHALL/FLUSHDB clears entire database. Extremely dangerous in production.
Fix Action
Use pattern-based deletion with SCAN instead
Applies To
- */.ts
- */.js
Redis Client Without Retry
Id
cache-no-connection-retry
Severity
info
Type
regex
Pattern
- new Redis\\([^)]\\)(?!.retry)
- createClient\\((?!.*retry)
Message
Redis client may not have retry logic configured.
Fix Action
Configure retry strategy for connection resilience
Applies To
- */.ts
- */.js
Synchronous Cache Operations
Id
cache-sync-operations
Severity
warning
Type
regex
Pattern
- \\.getSync\\(
- \\.setSync\\(
- Sync\\(.*cache
Message
Synchronous cache operation may block event loop.
Fix Action
Use async operations: await cache.get()
Applies To
- */.ts
- */.js
HTTP Cache Without Vary Header
Id
cache-http-no-vary
Severity
info
Type
regex
Pattern
- Cache-Control.public(?!.Vary)
- max-age.(?!.Vary)
Message
Public cache without Vary header. May serve wrong content to users.
Fix Action
Add Vary header for content that differs by request headers
Applies To
- */.ts
- */.js
Private Data with Public Cache
Id
cache-private-data-public
Severity
warning
Type
regex
Pattern
- user.Cache-Control.public
- profile.max-age.(?!.*private)
- account.Cache-Control.public
Message
User-specific data may be cached publicly. Use private cache.
Fix Action
Use 'Cache-Control: private' for user-specific data
Applies To
- */.ts
- */.js