Flash-Sale Price Cache Race
A retail platform runs six identical API servers behind a load balancer. Product price reads go through Redis using cache-aside: on a miss, the server reads PostgreSQL, returns the value, and writes it to Redis with a 60-second TTL. A flash sale updates 4,000 SKU prices in one transaction. The update service commits to PostgreSQL and then deletes each Redis key. During the first minute, about 3% of price reads return the old price, and some users see the old price for up to 90 seconds. Traffic is 12,000 reads/min, cache hit ratio is 95%, and p99 database read latency is 40 ms. Logs show several 'cache write' events occurring a few milliseconds after the delete for the same key. The team proposes lowering TTL to 5 seconds and adding more Redis replicas.
1. Explain how the stale price is possible even though the update service deletes the cache key after committing.
Read the worked answer
The pattern is a cache-aside race. A read request misses the key just before the price update commits. It reads the old price from PostgreSQL. The update then commits and deletes the key. The read request, still holding the old value, writes it back to Redis after the delete. Other servers then serve that stale value until TTL expires.
The 90-second observation is longer than the 60-second TTL, which points to a second factor: if Redis replicas are read asynchronously, a replica may serve an old cached value briefly, or the same race may repeat on a hot key. More replicas alone make this worse, not better: asynchronous replication increases the window in which a deleted key can still be read. Lowering TTL would shorten the maximum stale window but would not remove the race.
2. Evaluate the proposal to cut TTL to 5 seconds and add Redis replicas. Recommend a correctness-focused fix and state the trade-off you accept.
Read the worked answer
Do not lower TTL to 5 s as the primary fix. At 12,000 reads/min, a 95% hit ratio means about 600 database reads/min. If TTL falls from 60 s to 5 s, hit ratio may drop sharply; even a fall to 85% would mean 1,800 database reads/min, tripling load during the sale. More replicas also risks stale reads.
Better fixes, in order: (1) make price updates write-through: within the same transaction, update PostgreSQL and set the new value in Redis, with a version number; (2) use a per-key lock or single-flight on cache fill, so only one reader can populate a key after a miss; (3) for read-your-writes, route reads for recently updated SKUs to the primary for a short window, or store a version and reject stale writes. A delayed double-delete is a cheaper stopgap: delete after commit and again after 1–2 s. Measure stale-read rate and database QPS before and after. The trade-off is slightly higher write complexity and some extra primary reads, in exchange for correctness.