Cache Invalidation Strategies: TTL, Write-Through and Stampedes
Advanced4 minInnovialabs editorial team
In short
For most systems, use cache-aside with a TTL and delete the key on every write; protect hot keys with a lock or single-flight, add jitter to TTLs and serve stale data while refreshing in the background.
The short answer
There is no perfect invalidation strategy, only a trade-off between freshness, load on the database and complexity. A solid default for most products:
- cache-aside reads: check the cache, on a miss load from the database and store the result;
- a TTL on every key as a safety net;
- delete the key after every write to the source of truth;
- protection against stampedes on hot keys.
Start by deciding, per type of data, how stale it may be. A product description can lag a minute; a balance or stock count at checkout cannot.
Write patterns compared
| Pattern | How it works | Good for | Risks |
|---|---|---|---|
| Cache-aside | App reads cache, fills it on miss, invalidates on write | Most read-heavy APIs | Race between write and refill |
| Write-through | Every write goes to cache and database together | Data read right after writing | Slower writes, cache fills with rarely read data |
| Write-behind | Write to cache, flush to database asynchronously | Very high write rates, counters | Data loss if cache fails before flush |
Write-behind is the most dangerous: the cache temporarily becomes the source of truth. Use it only where losing recent writes is acceptable or the queue is durable.
TTL vs event-based invalidation
TTL is simple and self-healing: even if you forget to invalidate, data becomes correct after expiry. The cost is a guaranteed window of staleness and periodic misses.
Event-based invalidation removes or updates keys when data changes: right after a write, from a message bus, or from database change data capture (CDC). Data is fresher, but you must know every key that depends on a record, and a lost event means stale data until something else fixes it.
In practice, combine them: events for freshness, TTL as a safety net for missed events.
Practical rules:
- On write, delete the key rather than overwriting it. Two concurrent writers that update the cache can leave the older value in place.
- Write to the database first, then delete the key.
- A reader can still put an old value back right after deletion. Short TTLs, a second delayed delete, or versioned keys (
product:42:v17) close this gap. - Cache "not found" results for a short time, so requests for missing IDs do not all hit the database.
Cache stampede and how to stop it
A stampede happens when a popular key expires and hundreds of requests miss at once, all running the same expensive query. The database slows down, more requests time out, and the problem feeds itself.
Techniques that work:
- Locking or single-flight: only one request rebuilds the value, the rest wait briefly or get the old value.
- TTL jitter: add a random spread to expiry, so keys created together do not expire together.
- Stale-while-revalidate: keep serving the expired value while one background task refreshes it.
- Early refresh: refresh hot keys shortly before they expire, either on a schedule or probabilistically on read.
- Warm-up after deploys or cache restarts for known hot keys.
A minimal lock with Redis:
def get_product(pid):
key = f"product:{pid}"
cached = redis.get(key)
if cached:
return json.loads(cached)
if redis.set(f"lock:{key}", "1", nx=True, ex=10):
try:
product = db.load_product(pid)
ttl = 300 + random.randint(0, 60) # jitter
redis.set(key, json.dumps(product), ex=ttl)
return product
finally:
redis.delete(f"lock:{key}")
time.sleep(0.05)
cached = redis.get(key)
return json.loads(cached) if cached else db.load_product(pid)
For HTTP responses, the same idea is built into the Cache-Control header: max-age=60, stale-while-revalidate=300 lets CDNs and browsers serve a slightly stale copy while fetching a fresh one.
How to choose
- Classify data by acceptable staleness: seconds, minutes, hours, never.
- Never-stale data (money, stock at checkout) is read from the database or validated there; the cache is only a hint.
- For the rest, use cache-aside with TTL and delete-on-write.
- Add events only where TTL staleness is visibly hurting users.
- Monitor hit rate, miss latency and database load. A falling hit rate after a release often means a key format changed.