Background
Sections
IntroductionRequirements & Problem AnalysisConstraints & AssumptionsEstimation TechniquesFunctional vs Non-Functional RequirementsMoSCoW PrioritizationSystem Design FundamentalsArchitecture DiagramClass DiagramComponent DiagramData Flow Diagram (DFD)ER Diagram (Entity-Relationship Diagram)High Level Design (HLD)Low Level Design (LLD)Sequence DiagramState DiagramUse Case DiagramData StorageDocument StoresFile StorageGraph DatabasesIn-Memory DatabasesKey-Value StoresNewSQLNoSQL DatabasesObject StorageSQL Databases (RDBMS)Time-Series DatabasesWide-Column StoresDatabase ConceptsACID PropertiesCAP TheoremConsistency ModelsIndexingNormalization & DenormalizationReplicationSharding & PartitioningTransactions & Isolation LevelsScalabilityAuto-Scaling & ElasticityConsensus & Leader ElectionLoad BalancingReplication & Read ReplicasSharding & PartitioningVertical vs Horizontal ScalingAvailability & ReliabilityBackup & Data DurabilityCircuit BreakerData ConsistencyDisaster RecoveryFault Tolerance & FailoverGraceful DegradationHigh AvailabilityNetworkingCDNDNSFirewalls & VPNHTTP & HTTPSLoad Balancer & Reverse ProxyTCP/IP & UDPWebSocketsCachingCache InvalidationCache Read/Write PatternsCaching LayersEviction PoliciesRedis vs MemcachedMessaging & CommunicationDead-Letter QueueIdempotencyKafka vs RabbitMQ vs SQSMessage QueuesPub/SubCompute & ServicesAPI GatewayContainers & OrchestrationMonolith vs MicroservicesServerlessService DiscoveryService MeshWeb Server & App ServerAPI DesignAPI Versioning & IdempotencyAuthentication & AuthorizationGraphQLgRPCPaginationRate Limiting & ThrottlingRESTSecurityAuthentication & AuthorizationData PrivacyEncryptionInput Validation & InjectionOAuth2 & JWTSecrets ManagementXSS & CSRFStorage & File SystemsBackup & RetentionBlock vs File vs Object StorageData Lakes & WarehousesDistributed File SystemsEphemeral StorageObservability & MonitoringDistributed TracingHealth ChecksLoggingMetricsSLI, SLO, SLADesign PatternsBulkhead PatternCircuit Breaker PatternCreational PatternsRate Limiter PatternRetry PatternStructural & Behavioral Patterns

Cache Invalidation

7 min read

In a Nutshell

"There are only two hard things in Computer Science: cache invalidation and naming things." The joke endures because cache invalidation — deciding when a cached copy is no longer valid and getting rid of it — is genuinely hard. Cache too aggressively and you serve stale data; invalidate too eagerly and you lose the benefit of caching. The core tension is between freshness (users see current data) and efficiency (fewer origin hits). Invalidation strategies range from simple time-based expiry (TTL) to explicit event-driven deletion, each trading complexity for control over exactly how stale your data can get.

2D minimalistic diagram showing a cached item with a timer (TTL) counting down; when the underlying database value changes, an "invalidate" arrow deletes the stale cache entry, so the next read fetches the fresh value from the database and repopulates the cache — illustrating the freshness-vs-staleness tension

How It Actually Works

The Three Ways Cached Data Goes Wrong

Problem Cause
Stale data Underlying source changed but cache still holds the old value
Cache stampede Many requests miss at once and overwhelm the origin
Inconsistency across nodes Local caches on different servers hold different versions

Invalidation strategy is mostly about controlling the first (staleness) without triggering the second (stampede).

Invalidation Strategies

Strategy How Freshness Complexity
TTL (time-based) Entry expires after N seconds Bounded staleness (up to TTL) Lowest
Explicit / event-driven Delete the key when the source changes Near-immediate Higher (must hook all writes)
Write-through Update cache on every write Always fresh Write overhead
Versioned keys Change the key when data changes Always fresh (old key ignored) Key management
Purge / bulk Clear a group of keys (e.g., by tag) Immediate Tracking dependencies

TTL: The Workhorse

The simplest and most robust strategy: give each entry an expiry. It self-heals (stale data can't live longer than the TTL) and needs no coordination with writes. The only decision is how long:

Short TTL (seconds):  fresher data, more origin load
Long TTL (hours):     less origin load, more staleness

Choose per data type by "how stale can this be?":
  Stock ticker      → 1s     (must be fresh)
  Product details   → 5–15m  (rarely changes)
  Country list      → 24h    (essentially static)

TTL alone is often enough — many systems never need explicit invalidation because bounded staleness is acceptable.

Explicit Invalidation: When You Need Freshness Now

When staleness up to the TTL is unacceptable, delete the key the moment the source changes. The safe pattern (from Cache Read/Write Patterns) is write to DB, then delete the cache key — the next read repopulates from fresh data. Combine with a short TTL as a safety net in case a delete is missed.

The Distributed Invalidation Problem

Explicit invalidation gets hard when caches are spread across many servers (especially local caches):

Server A and Server B each have a local copy of "user:42".
User updates their profile → Server A deletes its copy.
❌ Server B still serves the STALE copy.

Solutions:
  • Use a shared distributed cache (Redis) so there's ONE copy to invalidate.
  • Publish invalidation events (pub/sub) so every node evicts its local copy.
  • Keep local-cache TTLs very short so divergence self-heals quickly.

This is a strong argument for a shared cache tier over per-node local caches when freshness matters.

Stale-While-Revalidate: Freshness Without the Stampede

A powerful middle ground: serve the slightly-stale cached value immediately while asynchronously refreshing it in the background. Users never wait on a miss, and the cache stays reasonably fresh.

Request for expired-but-present key:
  → return stale value NOW (fast, no user-visible miss)
  → trigger a background refresh from the origin
  → future requests get the fresh value
Avoids both the latency spike AND the stampede of a hard expiry.

2D minimalistic diagram illustrating distributed cache invalidation via pub/sub: a database write triggers an invalidation message published to a central channel, which fans out to three application servers each holding a local cache, all three deleting their stale copy of the key simultaneously so no server serves outdated data

Seeing It in Action

Scenario: Invalidation strategy for a news site with mixed freshness needs.

Article body (rarely edited after publish):
  → TTL 10 min + explicit invalidation on edit.
    Editors' changes appear near-instantly (explicit delete);
    the TTL is a safety net if an invalidation event is lost.

Article view count (updates constantly, staleness OK):
  → TTL 30s, no explicit invalidation.
    A count that's 30s old is perfectly acceptable; not worth the
    complexity of invalidating on every increment.

Homepage / section fronts (curated, high traffic):
  → stale-while-revalidate.
    Serve the cached front instantly; refresh in the background.
    A viral spike can't stampede the origin because only one
    background refresh runs at a time.

Breaking-news banner (must be current):
  → very short TTL (5s) + explicit purge on publish.
    Purge propagated via Redis pub/sub to all edge/app caches.

Multi-server local caches:
  → Invalidation events published to a Redis channel; every app
    server subscribes and evicts its local copy on an edit.

The design principle: there is no single right invalidation strategy — you choose per data type based on how much staleness is acceptable and how expensive the source is. TTL handles the easy majority; explicit invalidation and pub/sub handle the freshness-critical minority; stale-while-revalidate handles the high-traffic case where you want both freshness and stampede protection.

Interview Questions

  1. Q: Why is cache invalidation considered hard? Hint: It's the tension between freshness and efficiency with no perfect answer: cache too long → stale data; invalidate too eagerly → lose caching benefit and risk stampedes. It's worse in distributed settings where many nodes hold copies that must all be evicted consistently, and you must hook every write path that could change the source data. Missing one path silently serves stale data.

  2. Q: When is TTL-based expiry sufficient, and when do you need explicit invalidation? Hint: TTL suffices when bounded staleness is acceptable — the data can be up to N seconds old without harm (view counts, catalogs, config). It's simple and self-healing. Use explicit invalidation (delete on write) when staleness up to the TTL is unacceptable and changes must appear near-immediately (edited content, price changes). Often combine both: explicit delete + short TTL safety net.

  3. Q: What's the safe ordering for invalidating a cache on a write, and why? Hint: Write to the database first, then delete (invalidate) the cache key — don't update it. Deleting avoids the races that updating causes (concurrent writers leaving a stale value), because a missed/spurious delete only triggers a harmless re-fetch. The next read repopulates from the fresh DB value.

  4. Q: How do you invalidate caches that live locally on many servers? Hint: Local per-node caches can't be deleted from one place. Options: use a shared distributed cache (Redis) so there's a single copy to invalidate; publish invalidation events via pub/sub so every node evicts its local copy; or keep local TTLs very short so divergence self-heals. Freshness needs favor a shared cache tier over per-node local caches.

  5. Q: What is stale-while-revalidate and what problem does it solve? Hint: On an expired-but-present entry, serve the stale value immediately while triggering an asynchronous background refresh. It eliminates the user-visible latency of a cache miss and prevents cache stampedes (only one refresh runs), giving near-fresh data without a hard-expiry cliff. Ideal for high-traffic, expensive-to-compute resources like homepages.

References

Dive Deeper