Course contents
On this page
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
- Reader misses the cache
It reads price 20, then pauses before filling the cache.
- Writer commits price 25
It deletes the cached entry.
- Reader resumes
It fills the now-empty cache with the old price 20.
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
- Learning outcomes
- A late cache fill can resurrect stale data
- Cache-aside and update strategies
- Hit rates and miss amplification
- Materialized views
- Incremental joins and dependencies
- CDC and progress boundaries
- CQRS and separate read models
- Retention, rebuilding, and evolution
- Solve the stale-fill race with a retained generation
- A versioned cache entry needs a meaningful version order
- Delta maintenance needs old and new contributions
- Incremental joins can have large deltas
- CDC progress is a transactional relationship
- Rebuilds need a snapshot-and-catch-up protocol
- Caching moves work across time and ownership
- A stale-fill timeline makes the race visible
- Case connection and design conclusion
- Sources and further reading