Course contents
On this page
Lesson 52 of 59

An old cache fill can arrive after an update

Trace a stale-value race and explain why deleting a cached value does not cancel an in-flight read.

A product page reads price 20 from the database and keeps it in a cache for faster reuse. A cache is a saved copy that can usually be reconstructed from the authoritative data. On a miss, the application reads the database and fills the cache.

When the price changes to 25, the writer deletes the cached value. That sounds sufficient. Now overlap the writer with a slow cache fill.

Follow the value that was already on its way

  1. Reader misses the cache

    It reads price 20, then pauses before filling the cache.

  2. Writer commits price 25

    It deletes the cached entry.

  3. Reader resumes

    It fills the now-empty cache with the old price 20.

Deleting the entry removed stored data. It did not revoke the reader's permission to publish an older result.

Future readers can now see 20 even though deletion happened after the update. The missing information is that this fill began before a newer change.

Retain a generation through invalidation

For this specific race, associate the cache key with a generation number. The reader remembers generation 7 before fetching. After committing the update, the writer atomically advances the cache generation to 8 and clears the value. A fill may publish only if the generation still equals the number it remembered; the check and publication are one atomic cache operation.

The delayed reader offers generation 7. The cache now holds 8, so it rejects the fill. Keeping the generation matters: deleting it with the value could make an old request indistinguishable from a new one.

This mechanism closes the illustrated stale-fill race after the invalidation has completed. It does not make the database commit and cache invalidation atomic. A writer can crash between them. Cache restarts, lost generation records, and delayed invalidation delivery also need an explicit design.

For a product page, bounded staleness may be acceptable. For deciding whether a purchase is allowed, the transaction should consult authoritative state under the required concurrency rules. A cache hit is not permission to weaken that rule.

Predict a second update

A reader remembers generation 8. Two later invalidations advance it to 9 and then 10 before the fill arrives. Should the fill be accepted because 8 was once valid?

Compare the remembered and current generations

No. Publication requires equality with the current generation, which is 10. The reader must discard its result or retry through the chosen policy. The generation protects against older work arriving late; it is not a count of how useful the value looks.

A stored query result has the same broad dependency problem: changes to its inputs can make it stale. PostgreSQL calls a persisted query result a materialized view and provides explicit refresh operations. That is a related mechanism, not the generation protocol above.

Source: PostgreSQL 18 materialized views. The cache protocol is an educational example with the limitations stated above.

Extended reference

The extended chapter preserves the original technical coverage, additional examples, and citations. The explanation above supplies the concepts needed for this page; the reference develops the specialist cases.

Find a section from the original chapter