Invalidating Redis after a database COMMIT does not guarantee fresh cache data.
An update commits in PostgreSQL, the application executes DEL in Redis, and structured logs show operations completing in the expected order. Yet users continue receiving stale state because a concurrent read finished too late.
Request A encounters a cache miss and queries the database while the record is still at version 7. While Request A prepares to populate Redis, Request B writes version 8, commits the transaction, and deletes the cache key.
When Request A finally executes SET with the version 7 payload, it recreates the exact value that Request B just invalidated. From that moment on, subsequent requests encounter this stale data as a perfectly valid cache hit.
This is the critical blind spot: the sequence COMMIT → DEL establishes an ordering between writing and cache invalidation, but it does nothing to coordinate readers that began before the commit and write their cached values after the deletion.
Shortening TTLs bounds the duration of the inconsistency, but it does not close the concurrency window. Wrapping every read in a distributed lock simply trades inconsistency for lock contention and degraded latency.
A resilient strategy binds monotonic versions to cached data, allowing writes to Redis only when the payload version matches or exceeds the current generation, utilizing atomic compare-and-swap or Lua scripts rather than an uncoordinated SET.
Another pattern uses versioned keys, provided the pointer or manifest referencing the active version advances atomically with the database update. Without that consistency guarantee, versioned keys merely move the race condition elsewhere.
The choice depends on domain tolerance for stale reads, but the underlying principle remains unchanged: invalidating a cache key cannot maintain consistency as long as in-flight reads are permitted to recreate it with obsolete data.