Redis Cache Consistency
The moment you put a cache in front of a database, the problem of "the data in the cache doesn't match the data in the database" becomes unavoidable. This post organizes my understanding of cache consistency and the pitfalls of several common strategies.
Cache Consistency
First, let's talk about what the cache consistency problem is, why it needs solving, and what the options are.
Cache consistency problems arise when using a cache: because the cached data and the database data can diverge, you may end up with incorrect data or even data loss. This is especially common under high concurrency, where multiple threads reading and writing the same data make consistency hard to guarantee. The question is how to keep the data in Redis in sync with the data in the database and prevent inconsistency (in high-concurrency scenarios) — in some situations, inconsistent data can cause serious trouble, so the solution matters a great deal.
The root of the problem: the cache and the database are two independent stores, and writing to both is not an atomic operation. No matter which one you write first, there is a time window between the two steps, and any thread reading during that window may see a "half new, half old" state. Add in the unpredictability of thread scheduling, the timing of database transaction commits, and network latency, and all sorts of interleavings become possible. So discussing consistency strategies is really discussing: can this window be eliminated, and if not, how long can stale data survive — and can the business tolerate it?
Below are the common strategies I've collected, along with the problems each one has under high concurrency.
Write Directly to the Cache (Not Recommended)
The most intuitive approach: right after updating the database, write the latest value into Redis. The cache always has a value, and reads never fall through to the database.

During steps 3 and 4, under high concurrency there is no guarantee that the threads writing to Redis will execute in order (because of CPU time slicing — and it gets even worse with distributed, microservice deployments). As a result, the newest data may be overwritten by older data from another thread, and if no further writes happen afterward, the cache can be stuck holding stale data indefinitely.
In other words: thread A writes to the database first and thread B second, but the writes to Redis can easily land in the reverse order — B first, then A — leaving A's stale value in the cache. Worse, this overwrite has no self-healing mechanism: unless a new write comes along later to "flush" that key, the stale data stays in the cache and keeps getting read.
Some people reach straight for a distributed lock here (a JVM lock is enough for a single application). The lock has to cover the entire flow from the database write through the Redis write, you have to manage the function scope for each key operation yourself, and throughput definitely takes a serious hit — though you can tune the lock granularity. A distributed lock is best suited to strong-consistency scenarios, trading performance for consistency (recommended when strong consistency is required).
Delete the Cache After Writing the Database (Single Delete — Not Recommended)
Since "writing the cache" has the overwrite problem, flip the approach: after updating the database, just delete the cache entry and let the next read go back to the database and load the fresh value. Deletion is idempotent, so there's no risk of a new value being overwritten by an old one.

Single delete looks like it neatly avoids the overwrite problem, but things break once reader threads enter the picture. While a reader is fetching data from the database, another thread may be in the middle of committing a database transaction; the reader then gets the pre-commit stale value and stores it in the cache, causing inconsistency.
The exact interleaving goes like this: the reader thread gets a cache miss and queries the database, seeing the old value from before the write transaction commits; the writer thread then commits the transaction and deletes the cache; finally, the reader writes the stale value it's holding back into the cache. The delete happened before the stale value was backfilled, so it accomplished nothing — the cache is dirty again, and once again there is no self-healing.
Delayed Double Delete (Recommended)

By adding a delay queue and deleting the key a second time after a certain wait, this strategy fixes the problem single delete leaves behind — without any locking. That's why I personally think delayed double delete is a better fit for applications that can tolerate brief inconsistency in some of their data.
The point of the second delete is to act as a safety net: the interleaving above, where the stale value gets backfilled after the first delete, is cleaned up by the delayed second delete when it fires. The delay needs to cover the full duration of "reader queries the database + writes back to the cache" — in practice you estimate a generous value based on the actual latency of the endpoint. The cost is equally clear: within the delay window, reads may still return stale data, so what this guarantees is eventual consistency, not strong consistency.
Beyond the strategies above, there are plenty of other options:
- Read-write locks. Acquire a write lock during write operations so no other thread can read or write the cache until the write completes. This guarantees consistency but may add read latency and lock contention.
- Version numbers. Store a data version number in the cache and bump it with every update. On reads, compare version numbers and reload the data if they don't match. This guarantees consistency but costs extra storage and read overhead.
- Caching with TTLs. Set an expiration time when storing data in the cache; once it expires, the entry is invalidated automatically and must be reloaded. This reduces the likelihood of cache/database divergence, but may add storage and read overhead.
- Asynchronous updates via the database. When writing to the database, don't update the cache directly; instead, publish the update to a message queue and let a consumer update the cache. This offloads the database, but adds message-queue complexity and latency.
Pitfalls and Caveats
-
Whichever delete-based strategy you pick, give cache entries an expiration time as a backstop. Even if some interleaving produces dirty data, expiration will eventually force a reload from the source and correct it, capping how long the inconsistency can survive.
-
If the second delete in delayed double delete is done in memory (e.g., a thread sleeps and then deletes), it's lost when the application restarts. If reliability matters, dispatch the delete task through a message queue or delay queue so failures can be retried.
-
The cache delete itself can fail (network hiccups, Redis briefly unavailable). Failed deletes need a retry mechanism, or dirty data will likewise linger indefinitely.
-
Don't try to achieve "cache and database perfectly consistent at every instant" in ordinary business logic. The cache and the database are two separate stores; without locks or transaction-level coordination, strong consistency is simply unattainable. First figure out how much inconsistency your business can tolerate and for how long — then pick a strategy.
Wrapping Up
There is no silver bullet for cache consistency; it's fundamentally a trade-off between consistency, performance, and implementation complexity. Writing directly to the cache gets overwritten under concurrency with no self-healing; single delete plugs the overwrite hole but can't stop stale values from being backfilled; delayed double delete adds one deferred deletion as a safety net, buying eventual consistency — good enough for most businesses that can tolerate brief dirty reads. For truly strong-consistency scenarios, just take the lock and trade performance for correctness. Before choosing, answer one question: if this data is stale for a few hundred milliseconds, does the business actually hurt?
COMMENTS