Cache invalidation explained: why your app shows stale data

By Unlearnt

Cache invalidation means making a saved copy unusable when it should no longer be trusted. Updating a database does not automatically update every copy of its data. That is how a shop can save a new price successfully while its product page still shows the old one.

A cache holds a copy, not a promise of freshness

In cache-aside, the app checks the cache first. On a miss, it reads the database, saves the result in the cache and returns it. Later requests can avoid that database read. The benefit comes with a question: when the database changes, how will readers stop using the copy?

Invalidation, expiry and eviction are related but different. Invalidation rejects an entry because it should no longer be used. Expiry stops using it after its configured lifetime. Eviction can remove a perfectly current entry to make room. A cache miss alone does not tell you which happened.

[1]

Worked example: the £40 lamp that now costs £45

Imagine a product price cached at 10:00 with a fixed five-minute time to live (TTL). At 10:01, the database price changes from £40 to £45. Assume reads do not extend the TTL and there are no other caches or overlapping writes.

With expiry alone, a 10:02 visitor can still see £40. At 10:05, the entry expires; the next read fetches £45. That is up to four minutes of stale reads after this particular update, not five minutes after every visit.

With explicit invalidation, the writer commits £45 to the database and then removes the corresponding cache entry. The next cache miss fetches £45. This improves freshness, but a failed invalidation or an overlapping request can still matter.

One database update, two cache policies. Simplified timeline with no concurrent reads.
TimeExpiry onlyInvalidate after the write
10:00Cache £40 until 10:05Cache £40 until 10:05
10:01Database £45; cache still £40Database £45; cached price removed
10:02Read returns £40Miss fetches and caches £45

Deleting after a write does not eliminate every race

Suppose the cache is empty. Reader A fetches £40 from the database, then pauses before caching it. Writer B commits £45 and deletes the cache key. Reader A resumes and caches its earlier £40 result. The deletion succeeded, yet the stale price is back.

Deleting before the database write creates another race: a read can refill the cache with the old database value before the update commits. Writing first avoids that sequence, but not the delayed-reader sequence above. Correctness depends on the whole ordering, not just adding a delete call.

[1][2]

Choose freshness requirements before choosing a TTL

For a public catalogue, a short delay may be acceptable. For the price charged at checkout, validate against the authoritative price and make any change clear to the customer. A faster product page should not silently decide a different transaction price.

Use targeted invalidation for affected keys, with expiry as a backstop. Product details may also appear in category lists, search results or local process caches: deleting one key does not refresh those automatically. More demanding systems can use coordinated version checks or ordering mechanisms to reject outdated fills. A version field by itself is not enforcement.

Redis client-side caching is one concrete approach: the server tracks relevant reads and sends invalidation messages when keys change. This is specific coordination between Redis and its clients, not automatic synchronisation with an unrelated database.

[3]

When an old value keeps coming back

Trace a single record before clearing everything.

  • Record the database value and version, cache key, cached value and expiry time.
  • Check which write committed, which keys it invalidated and whether that operation succeeded.
  • Look for requests that read an older value before the write but cached it afterwards.
  • Check for another layer serving the response, such as a CDN or per-process cache.
  • Test the actual requirement: eventual freshness, read-your-own-write, or a stronger guarantee.

Check your understanding

Optional: apply the idea to a new scenario.

A cached delivery fee expires after 60 seconds. Reader A fetches the old £3 fee from the database but pauses before caching it. Writer B changes the fee to £4 and deletes the cache key. A then caches £3 with a fresh 60-second TTL. Why can the next visitor still see £3, and what would you check before trusting that fee at checkout?

Answer with the tutor →

Free to start, no account needed.

Sources

  1. Microsoft Azure Architecture Center: Cache-Aside pattern
  2. Redis: cache consistency strategies
  3. Redis documentation: client-side caching