Write-through caching sounds bulletproof: every write to the database also updates the cache immediately, so the cache always holds fresh data. Yet teams that adopt it often discover higher write latency, wasted capacity on cold keys, and surprising inconsistency under concurrent writes. The problem isn't caching itself — it's the assumption that preemptive invalidation must be eager. Lazy eviction, where stale entries are tolerated until evicted by time-to-live (TTL) or access pattern, frequently outperforms aggressive drain strategies in throughput, simplicity, and operational cost. This guide explains why, and how to decide which approach fits your workload.
Who Should Rethink Write-Through — and What Goes Wrong Without It
Write-through is most appealing to teams with strong consistency requirements — financial transactions, inventory systems, or any scenario where serving stale data could cause visible errors. The intuition is straightforward: if the cache mirrors the database on every write, reads are always fresh. But this intuition breaks down under real-world constraints.
Consider a typical e-commerce product catalog. A single product might be read thousands of times but updated only a few dozen times per day. With write-through, every update — even to fields like 'description' that are rarely queried — triggers a cache write. The cache fills with entries that are never read between updates, wasting memory and increasing write latency because the application must wait for both database and cache writes to complete. Worse, if the cache cluster experiences a transient failure during a write, the application may need to implement complex retry or fallback logic to avoid data loss.
Another common pain point is the thundering herd problem during cache warm-up. With write-through, a cold cache (after a restart or deployment) must be populated by writes, not reads. Until the first write occurs, the cache serves no data, forcing all reads to hit the database. In contrast, lazy eviction with a read-through or cache-aside pattern populates the cache on first read, gradually warming without a sudden load spike.
The core fallacy is conflating 'freshness' with 'performance.' Write-through optimizes for read consistency at the cost of write latency and cache efficiency. For workloads where writes are infrequent relative to reads — the majority of web applications — this trade-off is suboptimal. Lazy eviction, combined with a reasonable TTL, provides near-equivalent consistency for most use cases while keeping write latency low and cache utilization high.
Teams that ignore this trade-off often end up with overprovisioned cache clusters, complex write pipelines, and operational incidents during cache failures. The alternative is not to abandon consistency but to choose the right eviction strategy for each data type.
Prerequisites: What You Need Before Choosing an Eviction Strategy
Before evaluating write-through versus lazy eviction, you need a clear picture of your workload's access patterns, consistency requirements, and infrastructure constraints. Here are the key factors to settle first.
Understand Your Read-to-Write Ratio
Measure the ratio of cache reads to writes for each data set. A ratio above 10:1 generally favors lazy eviction, because the cost of eager cache writes for every update outweighs the benefit of immediate freshness. For ratios below 2:1 — for example, a real-time analytics pipeline where data is written as often as it is read — write-through may be justified.
Define Consistency Windows
Not all data needs immediate consistency. A product price might need to be accurate to within seconds, while a user's profile picture can tolerate minutes of staleness. Document the acceptable staleness window for each data category. Lazy eviction works well when the TTL can be set to a value within that window. Write-through is only necessary when the window is effectively zero — and even then, you may need distributed locks or transactions to avoid race conditions.
Assess Cache Failure Modes
Write-through caches are more sensitive to cache downtime. If the cache becomes unavailable, write-through applications must either fail the write or queue it, adding complexity. Lazy eviction with a cache-aside pattern can simply skip the cache on writes and continue serving stale data from the cache until it recovers, degrading gracefully.
Evaluate Operational Overhead
Write-through often requires custom middleware or database triggers to keep the cache synchronized. Lazy eviction can be implemented with a simple TTL and a read-through callback. Consider your team's capacity to maintain additional infrastructure. For many teams, the operational simplicity of lazy eviction is a decisive advantage.
Once you have these parameters documented, you can map each data set to a caching strategy. The remainder of this guide assumes you have at least one data set where lazy eviction is a candidate — and you want to implement it correctly.
Core Workflow: Implementing Lazy Eviction with Read-Through
Lazy eviction is most effective when paired with a read-through or cache-aside pattern. Here's the step-by-step workflow we recommend.
Step 1: Set a Realistic TTL
Choose a TTL that aligns with your consistency window. For a news feed, 60 seconds might be acceptable; for a stock ticker, 5 seconds. Avoid extremely short TTLs (under 1 second) unless you have a dedicated low-latency path, as they can cause cache thrashing.
Step 2: Implement Cache-Aside Reads
On a read request, check the cache first. If the key exists and has not expired, return it. If the key is missing or expired, fetch the data from the database, write it to the cache with the configured TTL, and return it. Use a distributed lock or a compare-and-set operation to prevent multiple concurrent reads from all hitting the database simultaneously (the thundering herd problem). Many cache libraries, such as Spring Cache or Django's cache framework, support this pattern natively.
Step 3: Handle Writes Without Cache Invalidation
On a write, update the database only. Do not update or delete the cache entry synchronously. The stale cache entry will be evicted by TTL or replaced on the next read. This avoids write amplification and keeps write latency low. If you need to serve fresh data sooner, you can optionally set a shorter TTL on the updated key or use a background job to invalidate it — but this is an optimization, not a requirement.
Step 4: Monitor Cache Hit Ratio and Staleness
Track cache hit ratio and average staleness (time between database update and cache refresh). If hit ratio drops below 80%, consider increasing TTL or pre-warming popular keys. If staleness exceeds your consistency window, reduce TTL or switch to write-through for that data set.
Step 5: Plan for Cache Warm-Up
After a cache flush or deployment, the cache will be cold. Lazy eviction handles this gracefully: the first read for each key populates it. To avoid a thundering herd on a popular key, use a background warm-up script that reads the most accessed keys from the database and writes them to the cache with a staggered TTL (e.g., random offset of ±10% of the base TTL) to prevent mass expiration.
This workflow is simple, robust, and scales well for read-heavy workloads. The key insight is that you don't need to chase perfect freshness — you only need freshness good enough for your users.
Tools and Setup: Choosing the Right Cache Infrastructure
Lazy eviction works with most caching systems, but some tools make it easier. Here's a comparison of popular options.
Redis with TTL and Lazy Expiration
Redis natively supports TTL and lazy expiration: expired keys are removed only when accessed or when the background expiry cycle runs. This aligns perfectly with lazy eviction. Use the EXPIRE command on set, and let Redis handle eviction. For read-through, implement the logic in your application layer or use Redis modules like RediSearch for secondary indexing.
Memcached with Explicit TTL
Memcached also supports TTL, but it does not have built-in read-through callbacks. You'll need to implement the cache-aside pattern in your application. Memcached is simpler and faster for pure key-value lookups, but lacks Redis's data structures and persistence options.
CDN Caches (e.g., CloudFront, Fastly)
For edge caching, lazy eviction is the norm. CDNs cache content with TTL and revalidate on demand. You can use Cache-Control headers to set TTL and stale-while-revalidate to serve stale content while fetching fresh data in the background. This is essentially lazy eviction at the edge.
Database-Backed Caches (e.g., PostgreSQL with pgpool)
Some teams use the database itself as a cache with materialized views or query result caching. Lazy eviction here means refreshing the materialized view on a schedule rather than on every write. This works well for aggregation queries that are expensive to compute.
Whichever tool you choose, ensure it supports atomic compare-and-set or locking to prevent thundering herds on cache misses. Most modern cache clients provide this out of the box.
Variations for Different Constraints
Not every workload fits the standard lazy eviction model. Here are variations for common constraints.
High Write Volume with Read-Through
If writes are frequent but reads are even more frequent, you can combine lazy eviction with write-back (write to cache first, then asynchronously to database). This reduces write latency further but risks data loss on cache failure. Use this only when you can tolerate some data loss or have a reliable persistence layer for the cache.
Strict Consistency with TTL
If you need strong consistency but still want to avoid write-through, consider using a short TTL (e.g., 1 second) combined with a read-repair mechanism: on each read, check if the cached value's timestamp is older than the database's last update. This adds a database query on every read but avoids writing on every write.
Multi-Region Deployments
In a multi-region setup, lazy eviction with a global TTL can cause cross-region staleness. Use a per-region TTL and a global invalidation bus (e.g., Kafka or Redis Pub/Sub) to propagate invalidations lazily — the invalidation message sets a short TTL in the remote region rather than deleting the key immediately.
Cache for Aggregates or Computed Data
For data that is expensive to compute (e.g., a user's friend list with scores), lazy eviction with a longer TTL works well. You can also use a background job to recompute the aggregate before the TTL expires, ensuring the cache is always warm for popular keys.
Each variation adjusts the trade-off between consistency, latency, and complexity. The common thread is that you avoid synchronous cache writes on every database update.
Pitfalls, Debugging, and What to Check When It Fails
Even with a solid design, lazy eviction can fail in subtle ways. Here are the most common issues and how to diagnose them.
Stale Data Served Beyond Acceptable Window
If users see outdated data, first check your TTL configuration. A common mistake is setting TTL too long. Reduce it incrementally until staleness is within tolerance. Also check if the cache is being bypassed due to a bug in the read-through logic — for example, a missing cache write on miss.
Thundering Herds on Cache Miss
When a popular key expires, multiple concurrent requests may all miss the cache and hit the database simultaneously. Mitigate this with a mutex lock or a 'probabilistic early expiration' strategy: if the remaining TTL is below a threshold, allow only one request to refresh the cache while others wait or serve stale data.
Cache Pollution from Rarely Read Keys
If your workload includes many one-off queries (e.g., user-specific reports), lazy eviction can fill the cache with entries that expire before being read again. Use an LRU eviction policy in addition to TTL, or skip caching for keys that are unlikely to be reused.
Inconsistent TTL Across Replicas
In a clustered cache, if TTL values differ across replicas (e.g., due to clock skew), some nodes may serve stale data while others serve fresh. Ensure all nodes use NTP and consider using a centralized TTL value stored with the key.
Write Skew with Lazy Eviction
If two concurrent writes update the same key in the database, and the cache is not invalidated, the cache may reflect an older write. This is acceptable if the TTL is short and the order of writes doesn't matter. For critical data, use a version field and reject stale cache updates.
When debugging, start by monitoring cache hit ratio and database load. A sudden drop in hit ratio often indicates a TTL that is too short or a bug in the read-through path. Use cache tracing tools (e.g., Redis MONITOR or memcached stats) to see which keys are being evicted and when.
FAQ and Checklist: Making the Decision
Frequently Asked Questions
When should I absolutely use write-through? Write-through is justified when stale data can cause immediate financial or safety harm, such as in a payment processing system where reading an old balance could allow double spending. Even then, consider using a distributed lock or optimistic concurrency control rather than write-through alone.
Can I combine write-through and lazy eviction? Yes. Use write-through for critical data (e.g., inventory counts) and lazy eviction for less critical data (e.g., product descriptions). This hybrid approach is common in practice.
Does lazy eviction work with cache invalidation on update? It can, but that moves toward write-through. If you need immediate invalidation, consider setting a very short TTL (e.g., 1 second) on the updated key instead of deleting it. This gives you near-immediate freshness without synchronous cache write overhead.
How do I handle cache stampedes? Use a mutex lock or a 'dogpile' prevention library. Some caches support 'get with timeout' that extends TTL on access, reducing the chance of mass expiration.
Decision Checklist
- Measure read-to-write ratio: if >10:1, prefer lazy eviction.
- Define acceptable staleness window for each data set.
- Assess cache failure impact: can your app degrade gracefully?
- Choose a cache tool with TTL and atomic operations.
- Implement read-through with lock or compare-and-set.
- Set TTL within consistency window; monitor hit ratio.
- Test with a subset of traffic before full rollout.
By following this checklist, you can confidently move away from write-through for the majority of your caching needs, reducing latency and operational complexity while maintaining acceptable consistency.
The write-through fallacy is not that write-through never works — it's that it's often the default when a lazier approach would serve better. Start with lazy eviction, measure the results, and escalate to stronger consistency only where the data demands it. Your cache cluster — and your on-call team — will thank you.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!