A slow reader can put an old value back into the cache after the writer deleted the key. Here is the race, step by step:
- Reader A misses the cache and reads the old row from the database.
- Before A writes to the cache, writer B commits the new value and deletes the cache key.
- Then A writes its old value into the cache.
The cache now holds stale (old) data until the TTL (time to live) expires. The time window is small, but it really happens under load. It is more likely when read replicas lag behind the primary database.
Ways to make the window smaller:
- Keep TTLs short, so stale data cannot live for long.
- Use a delayed second delete: delete the key again a short time after the write.
- Store a version number or an updated-at timestamp with the value. Write to the cache only if your value is newer. Use a Lua script, so the compare and the write happen as one atomic step (compare-and-set).
- Drive invalidation from database change events, so the last delete happens after all commits.
None of these makes a cache strongly consistent. Choose based on how much stale data the feature can accept.
- A slow reader can refill the cache with old data after the writer's delete.
- Replica lag makes the race window bigger.
- A short TTL limits how long stale data can live.
- Use a delayed double delete, or a versioned compare-and-set in Lua.
- Caches give eventual consistency, not strong consistency.
Read more