Skip to content
Innovialabs

Cache Invalidation Strategies: TTL, Write-Through and Stampedes

Advanced4 min

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

PatternHow it worksGood forRisks
Cache-asideApp reads cache, fills it on miss, invalidates on writeMost read-heavy APIsRace between write and refill
Write-throughEvery write goes to cache and database togetherData read right after writingSlower writes, cache fills with rarely read data
Write-behindWrite to cache, flush to database asynchronouslyVery high write rates, countersData 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

  1. Classify data by acceptable staleness: seconds, minutes, hours, never.
  2. Never-stale data (money, stock at checkout) is read from the database or validated there; the cache is only a hint.
  3. For the rest, use cache-aside with TTL and delete-on-write.
  4. Add events only where TTL staleness is visibly hurting users.
  5. Monitor hit rate, miss latency and database load. A falling hit rate after a release often means a key format changed.

FAQ

Usually delete. Updating requires the exact same serialization and ordering everywhere and can leave older values after concurrent writes. Deleting lets the next read rebuild the value from the source of truth.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.