Best Practices

AI Rules for Caching Strategies

AI either caches nothing or caches everything without invalidation. Rules for cache-aside, TTL, invalidation strategies, cache stampede prevention, and multi-layer caching.

8 min read·November 28, 2024

AI either caches nothing or caches everything forever — both wrong

Cache-aside, TTL by data type, invalidation on write, stampede prevention, and multi-layer caching

The Two AI Caching Extremes — Both Wrong

AI generates code at two extremes: no caching (every request hits the database, every API call is fresh, every computation runs from scratch) or aggressive caching without invalidation (cache once, serve stale data forever, never update). Both are wrong. No caching wastes resources and creates unnecessary latency. Stale caching serves incorrect data — users see old prices, outdated profiles, and phantom items.

Good caching is about answering three questions: what to cache (data that is expensive to fetch and changes infrequently), how long to cache (TTL based on data freshness requirements), and when to invalidate (on writes, updates, and deletes that affect cached data). AI generates caching without answering any of these questions.

These rules cover: the cache-aside pattern (the most common application caching strategy), TTL configuration, invalidation strategies, cache stampede prevention, and multi-layer caching (CDN + application cache + database cache).

Rule 1: Cache-Aside Pattern for All Application Caching

The rule: 'Use cache-aside (lazy loading) as the default pattern: 1) Check cache. 2) If hit, return cached data. 3) If miss, fetch from source (database/API). 4) Store in cache with TTL. 5) Return data. const cached = await redis.get(key); if (cached) return JSON.parse(cached); const data = await db.query(...); await redis.set(key, JSON.stringify(data), "EX", 300); return data;'

For write-through: 'Update the cache on every write: when you update the database, also update the cache. This ensures the cache is never stale after a write. Combine with TTL for natural expiry of data that drifts from the source. Use for: user profiles, product details — data that is read frequently and updated occasionally.'

AI generates caching without a strategy: sometimes checking the cache, sometimes not, no TTL, no invalidation. Cache-aside is the foundation: every cache interaction follows the same check → miss → fetch → store → return pattern. One pattern, everywhere, consistently.

  • Cache-aside: check cache → miss → fetch source → store in cache → return
  • Always set TTL — never cache without expiry (data goes stale)
  • Write-through: update cache on every database write — cache is always current
  • Read-through: cache sits in front of DB — all reads go through cache first
  • Cache-aside is the default — write-through for frequently updated + read data
💡 One Pattern Everywhere

Cache-aside: check → miss → fetch → store → return. Every cache interaction follows this pattern. Consistency beats cleverness — one predictable pattern across every service is easier to debug than five different caching strategies.

Rule 2: TTL Based on Data Freshness Requirements

The rule: 'Set TTL based on how often the data changes and how stale it can be: user profile: 5 minutes (changes rarely, slight staleness acceptable). Product catalog: 1 minute (updates occasionally, freshness matters for pricing). Search results: 30 seconds (changes frequently, users expect fresh results). Session data: match session timeout. Static content: 1 hour or longer. Never cache without TTL — it creates permanent stale data.'

For stale-while-revalidate: 'Serve stale data while refreshing in the background: return the cached value immediately, trigger an async refresh, and update the cache for the next request. This eliminates latency for the user while keeping data reasonably fresh. Implement with: TTL for the fresh window + a grace period where stale data is served.'

AI either sets no TTL (cache forever — stale) or sets TTL: 1 (cache for 1 second — pointless). TTL should match the data: how often does it change? how bad is staleness? Set TTL to half the acceptable staleness window as a starting point.

Rule 3: Cache Invalidation on Writes

The rule: 'Invalidate (or update) the cache whenever the underlying data changes. Pattern: when you update/delete a database record, also delete the corresponding cache key. async function updateUser(id, data) { await db.users.update(id, data); await redis.del(`user:${id}`); // Invalidate the cached user return updatedUser; }. The next read triggers a cache miss → fresh fetch from DB.'

For key design: 'Use predictable, hierarchical cache keys: user:{id} for a specific user, user:{id}:posts for a user posts, users:list:page:{page} for paginated lists. Hierarchical keys enable: specific invalidation (delete user:123) and pattern invalidation (delete users:list:* — all list pages when a user is created/deleted).'

For the hardest problem: 'List invalidation is the hardest caching problem. When a new user is created, which list cache pages are stale? All of them — but invalidating every page is expensive. Solutions: TTL-based (lists expire quickly — 30s), event-based (publish user:created → invalidate list caches), or skip list caching entirely (cache individual records, rebuild lists from cached records).'

  • Invalidate on write: delete cache key when DB record changes
  • Hierarchical keys: user:{id}, user:{id}:posts, users:list:page:{n}
  • Pattern invalidation: redis.del('users:list:*') — all list pages at once
  • List cache is hardest: TTL-based (expire quickly) or event-based (pub/sub)
  • If invalidation is too complex, use shorter TTL instead — simpler and sufficient
⚠️ Lists Are the Hardest

When a user is created, which list pages are stale? All of them. But invalidating every page is expensive. Solution: use short TTL on lists (30s) and let them expire naturally. Cache individual records (long TTL) and rebuild lists from cached records.

Rule 4: Cache Stampede Prevention

The rule: 'Prevent cache stampede (thundering herd): when a popular cache key expires, hundreds of concurrent requests all miss the cache and hit the database simultaneously. Solutions: lock-based recomputation (only one request fetches, others wait), probabilistic early expiry (refresh before TTL expires — add random jitter), and stale-while-revalidate (serve stale, refresh async).'

For lock-based: 'When a cache miss occurs: try to acquire a lock (Redis SETNX). If lock acquired: fetch data, store in cache, release lock. If lock not acquired: wait briefly, then check cache again (another request is populating it). This ensures only one request hits the database — others wait for the cache to be populated.'

AI generates no stampede protection — every cache expiry triggers a DB flood. For high-traffic keys (homepage data, popular products, shared configuration), stampede protection is essential. One lock per cache key, one DB query per expiry cycle.

ℹ️ Stampede = DB Flood

Popular cache key expires → 1000 concurrent requests miss → 1000 identical DB queries. Lock-based recomputation: one request fetches, 999 wait for the cache to be populated. One lock prevents the entire stampede.

Rule 5: Multi-Layer Caching and Cache-Control Headers

The rule: 'Use multiple cache layers: CDN (Cloudflare, CloudFront — caches HTTP responses globally), application cache (Redis — caches computed data), and database cache (query cache, connection pool). Each layer has different TTL and invalidation: CDN: s-maxage=60 (1 minute, CDN-controlled), application: 5 minutes (Redis), database: query plan cache (automatic).'

For HTTP Cache-Control: 'Set Cache-Control headers on API responses: Cache-Control: s-maxage=60, stale-while-revalidate=300 (CDN caches for 60s, serves stale for 5min while revalidating). public for cacheable responses, private for user-specific responses, no-store for sensitive data (auth tokens, PII). Use Vary: Authorization for responses that differ by user.'

For CDN purging: 'Use cache tags or surrogate keys for granular CDN purging: Surrogate-Key: user-123. When user 123 updates their profile, purge: cdn.purge("user-123") — only that user's cached responses are invalidated, not the entire CDN cache. Without tags, CDN purging is all-or-nothing — expensive and slow.'

  • CDN: HTTP responses globally — s-maxage, stale-while-revalidate
  • Application: Redis — computed data, DB query results, session data
  • Database: query plan cache, connection pool — managed by the DB
  • Cache-Control: public/private/no-store — Vary: Authorization for user-specific
  • Surrogate keys for granular CDN purging — not all-or-nothing

Complete Caching Rules Template

Consolidated rules for caching strategies.

  • Cache-aside pattern: check → miss → fetch → store with TTL → return
  • TTL by data type: profile 5min, catalog 1min, search 30s — never no TTL
  • Invalidate on write: delete cache key when DB record changes — hierarchical keys
  • Stampede prevention: lock-based recomputation or stale-while-revalidate
  • Multi-layer: CDN (HTTP) + Redis (application) + DB cache (automatic)
  • Cache-Control: s-maxage for CDN, max-age for browser, no-store for sensitive
  • Surrogate keys for granular CDN purging — not all-or-nothing invalidation
  • Monitor: hit rate (target >90%), miss latency, eviction rate, memory usage