Cache Invalidation Race Conditions
A cache invalidation race occurs when concurrent reads, writes, and invalidations allow an older value to be placed in the cache after newer data already exists. The result is stale data that can persist until another refresh or expiration.
A common pattern is read-through caching: a request misses the cache, reads the database, and then stores the result in the cache. If a write updates the database while that read is in progress, the read may still hold an older snapshot and later write that stale value into the cache.
The race is especially easy to create when invalidation is separate from database modification. For example, request A reads an old value, request B updates the database and removes the cache entry, and request A subsequently inserts its old value into the now-empty cache. The cache has been repopulated with data that predates the database update.
A robust solution usually associates cached values with a version, timestamp, or other monotonic token. Before installing a value, the application can verify that it is still current, or use an atomic compare-and-set operation so an older version cannot overwrite a newer one.
Another approach is to update the cache as part of the write path rather than relying only on deletion. Distributed systems may instead use ordered events, versioned messages, write-through caching, or a cache-aside protocol combined with safeguards against stale repopulation.
The correct solution depends on the consistency requirement. TTL reduces the lifetime of stale data but does not by itself prevent the race; synchronization, version checks, or an ordering mechanism is needed when stale overwrites must be prevented.
Key points
- A cache miss can race with a concurrent database update and invalidation.
- An older read can repopulate an already-invalidated cache entry.
- TTL limits stale-data lifetime but does not eliminate the race.
- Version checks or atomic compare-and-set can prevent stale overwrites.
- Distributed caches require an explicit ordering or concurrency strategy when consistency matters.
Interview questions
How can a cache contain stale data immediately after an invalidation?
A request can miss the cache and begin reading an older value from the database. Another request can update the database and invalidate the cache while the first request is still running. The first request can then finish its old read and populate the empty cache with that stale value. Therefore, invalidation alone does not guarantee that a concurrent reader cannot repopulate stale data.
How would you prevent an older cache value from overwriting a newer value?
I would attach a monotonic version to each cached value and reject writes whose version is older than the version already stored. The comparison and update should be atomic, such as with compare-and-set in a local cache or an equivalent conditional operation in a distributed store. The version source must itself be appropriate for the consistency requirements, because timestamps can be problematic when clocks are not reliably ordered.
Why is TTL not a complete solution for cache invalidation races?
TTL controls how long an entry remains available, but it does not determine which concurrent write is newer. A stale read can still populate the cache after a database update and remain there until its TTL expires. TTL therefore bounds the potential lifetime of the stale entry rather than preventing the stale write itself.
What changes when the cache is distributed across several application instances?
A process-local lock is no longer sufficient because different instances can independently read, invalidate, and write the same key. The coordination mechanism must exist at a shared system boundary, such as an atomic operation in the cache or an ordered event stream. I would also consider network retries, duplicate messages, failures between database and cache operations, and whether version information travels with every update.
What consistency trade-offs should you discuss when designing cache invalidation?
First, define whether the application requires strong read-after-write behavior or can tolerate eventual consistency. Stronger guarantees generally require additional coordination, versioning, or a different cache-update architecture, which can increase latency and operational complexity. With eventual consistency, TTLs and asynchronous invalidation may be sufficient if the allowed stale-data window is understood and acceptable.