Design multi-tier caching architectures for web applications — cache-aside vs write-through vs write-behind, TTL design, cache invalidation, Redis patterns, CDN configuration, browser caching, and stampede prevention. Use when choosing a caching pattern, designing cache invalidation strategies, implementing Redis caching, configuring Cache-Control headers, or preventing cache stampedes. Activate on "cache invalidation", "cache-aside", "write-through", "TTL", "Redis cache", "CDN caching", "cache stampede", "stale data", "browser cache". NOT for database query caching within an ORM, memoization of pure functions, or CPU-level caching.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
La commande reste sur une seule ligne. Faites défiler horizontalement pour la vérifier avant de la copier.
Vous préférez une copie locale ? Téléchargez les fichiers actuellement disponibles dans SkillsMP.
Explorateur de fichiers
3 fichiers
Affichage de SKILL.md
SKILL.md
Instructions source · Aperçu en lecture seule
license
Apache-2.0
name
caching-strategies
description
Design multi-tier caching architectures for web applications — cache-aside vs write-through vs write-behind, TTL design, cache invalidation, Redis patterns, CDN configuration, browser caching, and stampede prevention. Use when choosing a caching pattern, designing cache invalidation strategies, implementing Redis caching, configuring Cache-Control headers, or preventing cache stampedes. Activate on "cache invalidation", "cache-aside", "write-through", "TTL", "Redis cache", "CDN caching", "cache stampede", "stale data", "browser cache". NOT for database query caching within an ORM, memoization of pure functions, or CPU-level caching.
{"category":"DevOps & Site Reliability","tags":["caching","strategies","cache-invalidation","cache-aside","write-through"],"pairs-with":[{"skill":"react-performance-optimizer","reason":"Client-side caching (memoization, SWR) complements server-side cache architecture"},{"skill":"performance-profiling","reason":"Profiling identifies cache miss hotspots that caching strategies then address"},{"skill":"cloudflare-worker-dev","reason":"Edge caching with Cloudflare KV/Cache API is a key tier in multi-level cache architectures"}]}
Caching is the most commonly misapplied performance technique. The failure mode is not "cache too little" — it is "cache without an invalidation strategy and then discover the problem in production six months later when users complain about stale data that you cannot explain."
When to Use
✅ Use for:
Choosing which caching pattern fits a use case (cache-aside, write-through, write-behind)
Designing TTL values for different data freshness requirements
Rule: Data mutates in one place first. Invalidation flows outward: DB → Redis → CDN. Never skip tiers in invalidation.
Cache-Aside Pattern (Most Common)
Application manages cache explicitly. On read: check cache, if miss fetch from DB, populate cache, return. On write: update DB, delete cache entry.
classUserCache {
privateredis: Redis;
privatereadonlyTTL_SECONDS = 300; // 5 minutesasyncgetUser(userId: string): Promise<User> {
const key = `user:${userId}`;
// 1. Check cacheconst cached = awaitthis.redis.get(key);
if (cached) returnJSON.parse(cached);
// 2. Cache miss — fetch from sourceconst user = await db.users.findById(userId);
if (!user) thrownewNotFoundError('User', userId);
// 3. Populate cacheawaitthis.redis.setex(key, this.TTL_SECONDS, JSON.stringify(user));
return user;
}
asyncupdateUser(userId: string, data: Partial<User>): Promise<User> {
const user = await db.users.update(userId, data);
// 4. Invalidate — delete, don't update// Updating in cache risks race conditions; let the next read repopulateawaitthis.redis.del(`user:${userId}`);
return user;
}
}
When invalidation deletes vs overwrites: Delete is almost always correct. Overwriting in cache after a write creates a race: another request may have fetched the old value between your DB write and your cache write. Delete forces the next reader to fetch fresh.
Write-Through Pattern
Every write goes to cache and DB synchronously. Cache is always populated. Good for data that is written once and read many times.
asyncfunctioncreateProduct(data: CreateProductInput): Promise<Product> {
// Write to DB first (source of truth)const product = await db.products.create(data);
// Immediately populate cache — no future cache miss for this productconst key = `product:${product.id}`;
await redis.setex(key, 3600, JSON.stringify(product));
// Also invalidate list caches that include this productawait redis.del('products:list:*'); // pattern delete via SCAN, see redis-patterns.mdreturn product;
}
Trade-off: Higher write latency (two writes per operation). Wasted cache space for items that are never read again after creation. Best for data with high read:write ratio.
TTL Design
TTL is not a cache invalidation strategy — it is a staleness budget. Design TTLs based on data volatility and acceptable staleness:
Data Type
TTL
Rationale
User session token
Match session expiry
Security requirement
User profile (name, avatar)
5-15 minutes
Changes rarely; short enough for responsiveness
Product catalog
1-4 hours
Changes occasionally; acceptable lag
Inventory counts
30 seconds
Changes frequently; short but not zero
Exchange rates
60 seconds
Regulatory; must not be too stale
Static config / feature flags
60 seconds + pub/sub invalidation
Needs push invalidation on change
Computed aggregates (daily stats)
Until next computation
Explicit invalidation on recalculate
TTL jitter: When many keys have the same TTL, they expire simultaneously, causing a thundering herd. Add random jitter:
A stampede (also: dog-pile, thundering herd) occurs when many requests simultaneously miss an expired cache key and all rush to compute or fetch the value.
Strategy 1: Probabilistic Early Expiry (XFetch)
Re-fetch before expiry with probability proportional to how close the key is to expiring:
Only one worker recomputes the value; others wait on the lock or return stale data:
asyncfunction getWithLock<T>(
key: string,
fetcher: () =>Promise<T>,
ttl: number
): Promise<T> {
const cached = await redis.get(key);
if (cached) returnJSON.parse(cached);
const lockKey = `lock:${key}`;
const lockAcquired = await redis.set(lockKey, '1', 'NX', 'PX', 5000); // 5s TTLif (!lockAcquired) {
// Another worker is computing — poll briefly then return stale or throwawaitsleep(100);
const retried = await redis.get(key);
if (retried) returnJSON.parse(retried);
thrownewError('Cache unavailable');
}
try {
const value = awaitfetcher();
await redis.setex(key, ttl, JSON.stringify(value));
return value;
} finally {
await redis.del(lockKey);
}
}
Consult references/redis-patterns.md for the Lua-atomic version of this lock (prevents lock release by wrong client).
Anti-Patterns
Anti-Pattern: Cache Everything Forever
Novice: "Caching makes things fast. Set TTL to 0 (no expiry) or a year to maximize cache hit rate."
Expert: Unbounded caches are memory leaks with extra steps. They also guarantee stale data — users see prices, permissions, and content from months ago. Production incidents traced to "why is this user seeing the old plan limit" are almost always cache-forever bugs.
// Wrong — no expiry means the cache grows foreverawait redis.set(`user:${id}`, JSON.stringify(user)); // no TTL// Right — every cache entry has a maximum lifetimeawait redis.setex(`user:${id}`, 300, JSON.stringify(user)); // 5 minutes
Python equivalent:
# Wrong
redis.set(f"user:{id}", json.dumps(user))
# Right
redis.setex(f"user:{id}", 300, json.dumps(user))
Detection: redis.set(key, value) without EX/PX/EXAT options. Redis TTL key returning -1 for cache keys. Memory growth over time with no plateau.
Timeline: This has always been wrong, but the Redis default of no-expiry makes it easy to do accidentally. Redis 7.0 (2022) introduced key eviction policies as default, reducing severity — but you still get stale data.
Anti-Pattern: No Invalidation Strategy
Novice: "I'll set a short TTL and the stale data problem solves itself."
Expert: TTL-only invalidation means every change to data has a propagation delay equal to the TTL. For some data (user roles, permissions, prices after a sale ends) that lag is unacceptable. Worse: this creates an implicit contract that is never documented, and teams later increase the TTL for performance without realizing they just made the staleness window much larger.
// Problem: user loses admin role, but can still access admin routes for 5 minutesawait redis.setex(`user:permissions:${id}`, 300, JSON.stringify(permissions));
// Right: invalidate explicitly on changeasyncfunctionrevokeAdminRole(userId: string) {
await db.userRoles.delete(userId, 'admin');
await redis.del(`user:permissions:${userId}`); // immediate invalidation// Also publish to notify other app instances to clear L1 cachesawait redis.publish('permissions:invalidated', userId);
}
LLM mistake: LLMs frequently omit invalidation logic in code generation because it is invisible in simple cache-aside examples. Every tutorial shows "set on write," few show "delete on update."
Detection: Cache sets with no corresponding deletes in write paths. TTL as the only eviction mechanism for user-controlled data (roles, permissions, settings). No DEL, UNLINK, or pub/sub events in the codebase's update handlers.