Cache Invalidation

how to detect and fix stale cache?

Ever wondered how applications like Instagram and Twitter(X) deliver content almost instantly? Caching makes this possible by reducing repeated database and network calls. However, cached data can quickly become outdated, and managing this staleness is one of the hardest problems in system design. Cache invalidation is the mechanism that solves it.

Cache invalidation answers one core question:

“When cached data becomes wrong, how do we detect it and fix it?”

Eviction removes data because of memory pressure.
Invalidation removes data because of data correctness.


Why cache invalidation is hard?

A cache creates two copies of data:

  • One in cache
  • One in the source of truth (DB)

The moment DB changes, cache risks becoming stale.

So every invalidation strategy is trying to balance:

  • Freshness
  • Performance
  • Complexity

This is why people say:

“There are only two hard things in computer science: naming things, cache invalidation, and off-by-one errors.”


1. Time-based invalidation (TTL)

This is the simplest and most common approach.

Idea

Cached data is valid only for a fixed duration.

After that:

  • Treat it as expired
  • Fetch fresh data

Characteristics

  • Simple
  • No coordination needed
  • Accepts staleness

We already implemented this here.

Key insight:
  • TTL is not eviction
  • TTL is invalidation triggered by time

When to use

  • Analytics
  • Feeds
  • Product listings
  • Weather
  • Any data that “eventually updates”

2. Write-based invalidation (invalidate on write)

This is the most important strategy in real systems.

Idea

Whenever data is updated in DB:

  • Invalidate corresponding cache entry

Flow

Update DB
→ Delete cache entry
→ Next read repopulates cache

This is usually paired with cache-aside.

JavaScript example

Assume:
  • DB update happens
  • Cache key is user:{id}
function updateUser(id, newData) {
  updateUserInDB(id, newData);

  // Invalidate cache
  cache.delete(`user:${id}`);
}
Next read:
  • Cache miss
  • Fresh DB value is cached

Characteristics

  • Strong consistency (after write)
  • Simple logic
  • Slightly higher read latency after writes

When to use

  • User profiles
  • Settings
  • Config data

3. Update-on-write (write-through style invalidation)

Instead of deleting cache, you update it immediately.

Flow

Update DB
→ Update cache

Example

function updateUser(id, newData) {
  updateUserInDB(id, newData);

  cache.set(`user:${id}`, newData);
}

Characteristics

  • No cache miss after write
  • Cache always fresh
  • Slightly more complex

Risk

  • If DB write succeeds but cache update fails → inconsistency

When to use

  • Read-heavy systems
  • Low tolerance for stale reads

4. Version-based invalidation

Instead of deleting data, you detect staleness.

Idea

Attach a version or timestamp to cached data.

Example cache entry

{
  value: user,
  version: 5
}
On read:
  • Compare version with DB
  • If mismatch → refresh cache

Example (simplified)

function getUser(id) {
  const cached = cache.get(id);
  const dbVersion = getUserVersionFromDB(id);

  if (cached && cached.version === dbVersion) {
    return cached.value;
  }

  const fresh = fetchUserFromDB(id);
  cache.set(id, { value: fresh, version: dbVersion });
  return fresh;
}

Characteristics

  • Very safe
  • More DB calls
  • Used in high-consistency systems

5. Event-based invalidation (distributed systems)

Used when:

  • Multiple services
  • Multiple caches

Idea

DB change emits an event:

  • All caches listening invalidate their entries

Example

UserUpdatedEvent(userId)
→ All services delete user:{id}

Technologies

  • Kafka
  • Pub/Sub
  • Redis streams

Characteristics

  • Scales well
  • Complex infra
  • Eventual consistency

6. Manual / administrative invalidation

Sometimes you just need a big red button.

Examples:

  • Price bug
  • Bad deployment
  • Emergency rollback
cache.clear();

Simple but powerful.


Cache stampede

Problem

TTL expires → 1000 requests → all hit DB at once.

Common mitigations

  • Lock per key
  • Request coalescing
  • Refresh-ahead
  • Jittered TTL

Example (conceptual):

if (isFetching(key)) {
  waitForResult(key);
} else {
  fetchAndPopulate(key);
}

Let me know in comments, if you would like to see implementation of this.


Mental model (this is key)

ConceptSolves
TTLStaleness over time
Invalidate-on-writeStaleness on updates
Update-on-writeCache freshness
VersioningConsistency correctness
Eviction (LRU)Memory pressure

Cache invalidation focuses on keeping data correct, but correctness alone isn’t enough in real systems. Memory is limited, and caches cannot grow indefinitely. Even perfectly valid data must eventually be removed to make room for newer or more frequently used entries. This is where eviction policies come into play. In the next section, we’ll look at how caches decide what to keep and what to discard under memory pressure, and why these decisions have a direct impact on performance and scalability. This blog is part of Caching 101 to Advanced series.


Discover more from I am Harisai

Subscribe now to keep reading and get access to the full archive.

Continue reading