Cache invalidation and the late-fill race
“Delete the cache key after the database update” leaves one timing problem: another request may already be loading the old value. Deletion removes the entry that exists now; it does not cancel a result that is still in flight.
Follow the two requests
Assume PostgreSQL holds revision 0 and an application uses cache-aside reads. This sequence can happen even when the writer invalidates after commit:
| Step | Reader A | Writer B |
|---|---|---|
| 1 | Misses the cache and reads revision 0 | |
| 2 | Pauses before storing the result | Updates the row to revision 1 |
| 3 | Commits and deletes the cache key | |
| 4 | Publishes its previously read revision 0 | |
| 5 | A later request reads the stale cached value |
A TTL can limit how long that value remains eligible. It does not make step 4 correct. Moving deletion before commit creates a different interval in which a reader can repopulate from the old committed database state. See the PostgreSQL and Redis guide for the boundary between application cache-aside and database-local row caching.
Validate publication as well as lookup
A cache fill needs evidence that its result is still eligible when it is
published. In pg_local_cache 2.0, writes fence affected keys or relations;
fills carry generation information, and positive cached entries carry tuple
visibility information. A changed generation can reject an old fill. A read
that cannot safely use an entry falls back to PostgreSQL.
These checks are part of a specific PostgreSQL read path. They do not turn the optional RESP endpoint into a general-purpose Redis server, and they do not invalidate values that an application has already copied elsewhere.
Test both rollback and commit
Use the two-session test on a disposable database. Warm the row in one session. In another, update the row while keeping the transaction open. Check three observations:
- The writer can read its own change through the source-table path.
- The other session still sees the committed value while the write is open.
- After rollback, the original value remains; after commit, a new statement
under
READ COMMITTEDsees the new revision.
The third condition refers to a new statement. A statement that started earlier does not have to adopt a newer snapshot midway through execution. The transaction contract also describes the modes that bypass the cache. Use ordinary SQL for row locking.
Check the next cache in the application
A request-local DataLoader can still hold a value it loaded before a mutation. Database invalidation cannot remove that JavaScript object. Clear or replace the affected loader entry after a mutation according to the application’s authorization and result contract. Keep loaders scoped to requests.
The batching guide separates request memoization from the shared row cache. When diagnosing a stale response, trace every storage point from the database snapshot to the response object. Correctness at one boundary does not clear the others.