Phil Karlton famously said there are only two hard things in computer science: cache invalidation and naming things. Cache invalidation—determining when cached data is stale and must be refreshed—is indeed challenging. Poor invalidation strategies lead to serving incorrect data or nullify caching benefits through excessive invalidation.
The Cache Consistency Problem
Caches improve performance by serving stale copies of data instead of querying the source of truth. But “stale” exists on a spectrum from perfectly acceptable to critically wrong. User profile data might tolerate minutes of staleness, while inventory counts need immediate consistency to prevent overselling.
The challenge is balancing freshness against performance. Aggressive invalidation keeps data fresh but increases latency and load on backend systems. Conservative invalidation maximizes cache benefits but risks serving outdated data. The right balance depends on your specific use case.
Time-Based Invalidation (TTL)
Time-to-live expiration is the simplest approach: cached items expire after a fixed duration. After expiration, the next access triggers a cache miss and refresh from the source. This requires no coordination between cache and data store and works for any data type.
TTL strategies work well when data has predictable update patterns and consistency requirements. Product information might use hour-long TTLs, while session data uses minute-long TTLs. Different data types can have different TTLs based on their characteristics.
The limitation is that TTL-based invalidation allows serving stale data for the entire TTL duration after updates. A product price change might not reflect in the cache for hours. This is acceptable for some use cases but problematic for others.
Event-Based Invalidation
Event-based invalidation explicitly invalidates cached items when the underlying data changes. When code updates a database record, it also invalidates or updates the corresponding cache entry. This provides much tighter consistency than TTL alone.
The application code that modifies data must also handle cache invalidation. For example, updating a user’s profile should invalidate the cached profile data. This couples application logic to cache implementation but provides precise control over consistency.
Challenges include ensuring every code path that modifies data includes appropriate cache invalidation. Missing an invalidation point leads to stale data that persists until TTL expiration. Code reviews and testing should verify cache invalidation accompanies all data modifications.
Write-Through and Write-Behind
Write-through caching automatically maintains consistency by writing to both cache and data store synchronously. The cache is always current because writes update it directly. This eliminates explicit invalidation logic but impacts write performance.
Write-behind caching is similar but asynchronous, improving write performance while maintaining eventual consistency. Both patterns centralize cache consistency management, reducing the risk of missed invalidations in application code.
Cache Stampede
Cache stampede occurs when a popular cached item expires and multiple requests simultaneously attempt to regenerate it. All requests experience cache misses, all query the database, and all try to populate the cache. This can overwhelm the database and defeat the purpose of caching.
Probabilistic early expiration addresses this by randomly refreshing items slightly before expiration, spreading regeneration load over time. Items with TTL remaining between 0 and some epsilon have a probability of being refreshed proportional to how close they are to expiration.
Request coalescing ensures only one request regenerates cached data when multiple requests encounter a miss. Subsequent requests wait for the first to complete and then use the newly cached value. This requires coordination among cache clients.
Lock-based approaches have the first request to discover a miss acquire a lock, perform the regeneration, and release the lock. Other requests wait for the lock, then find the data cached. This serializes regeneration but prevents duplicate work.
Granularity of Invalidation
Coarse-grained invalidation removes large chunks of cached data. Updating any product in a category might invalidate the entire category cache. This is simple to implement and never serves inconsistent data but invalidates more than necessary, reducing hit rates.
Fine-grained invalidation targets specific cached items. Updating one product invalidates only that product’s cache entry. This maximizes cache effectiveness but requires precise tracking of what cache entries depend on what data.
Tag-based invalidation associates cache entries with tags representing data dependencies. A product cache entry might have tags for the product ID, category, and brand. Invalidating a tag removes all entries with that tag. This provides flexible, multi-dimensional invalidation.
Dependency Tracking
Complex cached data often depends on multiple data sources. A product page might cache data from product, inventory, pricing, and review tables. When any dependency changes, the cached page should invalidate.
Explicit dependencies require the application to track what data sources each cache entry depends on and invalidate when any dependency changes. This is accurate but requires careful bookkeeping.
Conservative invalidation invalidates cached items that might be affected by a change, even if they’re not directly impacted. Updating any product might invalidate all cached category listings that could include that product. This is simple but reduces cache effectiveness.
Versioned Caching
Instead of invalidating entries, versioned caching creates new versions. Cache keys include a version identifier that changes when data updates. Old versions remain cached but new requests use the new version. This eliminates race conditions during invalidation and allows atomic cache updates.
The downside is increased memory usage as old versions remain until TTL expiration or eviction. For immutable or infrequently updated data, this tradeoff is worthwhile.
Multi-Level Caches
Applications often use multiple cache levels: browser caches, CDN caches, application-level caches, and database query caches. Invalidating all levels consistently is challenging.
Cache-Control headers manage browser and CDN caching for HTTP-based content. Setting appropriate max-age and must-revalidate directives controls cache behavior throughout the delivery chain.
Versioned URLs for static assets (like style.abc123.css) allow infinite caching because URL changes force fresh retrieval. This is incredibly effective for immutable content.
Best Practices
Start with simple TTL-based invalidation and add event-based invalidation for data requiring tighter consistency. Monitor cache hit rates to understand invalidation impact. Aggressive invalidation reduces hit rates, suggesting you might relax consistency requirements or increase TTL.
Implement cache invalidation as a separate concern, possibly using aspect-oriented programming or database triggers to ensure consistency. Test cache behavior explicitly: verify stale data eventually refreshes and updates reflect quickly enough for your requirements.
Document cache invalidation requirements for different data types so developers understand consistency expectations. Use monitoring to detect cache inconsistencies: if application behavior suggests incorrect cached data, investigate invalidation logic.
Cache invalidation is complex, but understanding the patterns and tradeoffs enables you to design caching strategies that balance performance and consistency. The goal isn’t perfect real-time consistency—databases provide that—but rather acceptable eventual consistency with dramatic performance improvements.