Redis3 min read

Redis Cache-Aside Can Reintroduce Stale Data

G

Guepard Team

November 14, 2026

Deleting a cache key after a database update is a common cache-aside pattern. It is not a proof that the next cached value will be current. A concurrent reader can fetch the old database value, pause, and write that old value into Redis after the writer has already invalidated the key.

The right design starts by defining the acceptable stale-read window. Some screens tolerate bounded staleness; authorization and inventory decisions often require authoritative reads.

The concrete problem

Assume a relational database owns a product record and Redis caches its display representation. This pseudocode illustrates the race. It intentionally does not present a naive invalidation pattern as a complete correctness solution.

Reader fetches old row; Writer commits and invalidates; Late fill can restore stale value

Figure: Reader fetches old row → Writer commits and invalidates → Late fill can restore stale value.

A worked example

# Reader R
cached = redis.get('product:42')
if cached is None:
    value = db.read_product(42)   # reads version 7
    # R pauses here
    redis.set('product:42', encode(value), ex=30)

# Writer W runs while R is paused
# db.update_product(42, ..., version=8)
# db.commit()
# redis.delete('product:42')
# R resumes and can cache version 7 after the delete.

Why the behavior matters

The database commit and Redis deletion belong to different systems. Even if the writer always deletes after commit, the reader's independent read-and-fill sequence can cross that boundary. A TTL bounds one dimension of staleness but does not eliminate the race or guarantee a strict maximum measured from the database update under every workload.

Possible designs include versioned cache values with guarded replacement, event-driven invalidation with explicit lag monitoring, or bypassing the cache for consistency-critical paths. Version-aware designs need an authoritative monotonic version and careful handling of absent keys: merely storing a version inside JSON does not prevent an older writer from winning.

Failure modes and design tradeoffs

A distributed lock around cache population can reduce duplicate fills but introduces lease, failure and ownership questions. It should not be mistaken for an atomic transaction with the source database. Delayed double deletion is a heuristic unless its timing assumptions are justified.

Negative caching has the same family of problems. A recently created record can remain hidden behind a cached “not found” entry. Choose shorter negative-cache lifetimes or invalidation behavior deliberately, and do not cache permission failures across users without a correctly scoped key.

What to verify before adoption

Implement a test barrier after the reader's database fetch, commit a newer value in the writer, invalidate, then release the reader. Assert the resulting cache state and the application's documented stale-read behavior. Include cache outage and delayed invalidation tests. Measure stale serves separately from cache hit rate; a high hit rate can hide incorrect results.

Primary reference

  • Redis documentation. Verify the exact deployed version and driver behavior before using the example in a production runbook.
G

Guepard Team

Guepard Engineering

Book a demo →