Why Caching Matters
Caching Trade-offs
Read aloud is unavailable in this browser.
The Two Hard Problems
There's a famous saying in computer science, often attributed to Phil Karlton:
"There are only two hard things in Computer Science: cache invalidation and naming things."
This quip has endured because it captures a fundamental truth: caching is deceptively difficult. The concept is simple—store data closer to where it's needed. The implementation is straightforward—a key-value lookup. But the consequences of caching ripple through your entire system, creating challenges in consistency, complexity, debugging, and operational management.
This page confronts the trade-offs head-on. Understanding what caching costs you—in complexity, in correctness risks, in operational burden—is essential for making informed decisions about when and how to cache. Caching should be a deliberate choice with full awareness of its implications, not a reflex applied everywhere.
By the end of this page, you will understand the consistency challenges caching creates, the complexity costs of cache management, the operational burdens of cached systems, and when caching is the wrong solution. You'll be equipped to make nuanced caching decisions.
The Cache Invalidation Problem
Cache invalidation is the process of removing or updating cached data when the underlying source data changes. It sounds simple: when data changes, update the cache. In practice, it's one of the most challenging problems in distributed systems.
Why invalidation is hard:
-
Distributed state — The cache and database are separate systems. There's no atomic operation that updates both simultaneously.
-
Timing windows — Between database update and cache invalidation, the cache contains stale data. Concurrent requests may read stale data or even re-cache it.
-
Network failures — Cache invalidation messages can fail, be delayed, or arrive out of order. The cache may never learn about updates.
-
Dependency tracking — A single data change may affect multiple cache entries. Understanding all dependencies is non-trivial.
-
Cross-system coordination — When multiple services share cached data, coordinating invalidation across services is complex.
Common invalidation failure modes:
Race condition during update:
Time 0: Cache has value A
Time 1: Process 1 updates DB to B
Time 2: Process 2 reads stale A from cache
Time 3: Process 1 invalidates cache
Time 4: Process 2 writes A back to cache (re-caching stale!)
Lost invalidation:
Time 0: DB updated to B
Time 1: Invalidation message sent
Time 2: Network drops message
Time 3: Cache still has A (indefinitely stale)
Out-of-order updates:
Time 0: DB updated to B, invalidation queued
Time 1: DB updated to C, invalidation queued
Time 2: Invalidation for C arrives, cache cleared
Time 3: Cache re-populated with C
Time 4: Invalidation for B arrives (late), cache cleared
Time 5: Cache re-populated with C (correct, but wasteful)
There is no general solution to cache invalidation that is simultaneously correct, fast, and simple. Every approach involves trade-offs. The best you can do is understand the trade-offs, choose appropriate for your use case, and design for the failure modes you're willing to accept.
Consistency Challenges
Caching inherently creates consistency challenges. By storing copies of data in multiple locations, you sacrifice the guarantee that all readers see the same value at the same time.
Types of inconsistency:
- •Stale reads — User reads outdated data from cache after an update. Most common issue. Severity depends on how stale and for how long.
- •Read-after-write inconsistency — User updates data, then reads and sees old value (their update not yet in cache). Confusing and frustrating UX.
- •Inconsistent views — Different parts of a page show data from different times. Product shows old price but new inventory, for example.
- •Lost updates — In write-through scenarios, concurrent writes may overwrite each other, with cache masking the conflict.
- •Resurrection of deleted data — Deleted data returns from cache after deletion because cache wasn't properly invalidated.
Staleness tolerance by domain:
Not all data requires the same consistency guarantees. Understanding your domain's tolerance for staleness helps you make appropriate trade-offs:
| Data Type | Staleness Tolerance | Recommended TTL | Notes |
|---|---|---|---|
| Financial transaction data | Zero tolerance | No caching / real-time | Legal and regulatory requirements |
| Account balances | Seconds | 1-5 seconds | User sees recent transactions quickly |
| User profile (self-view) | Immediate for writer | Cache-aside with invalidation | Read-after-write consistency needed |
| User profile (others' view) | Minutes | 5-15 minutes | Others can tolerate delay |
| Product catalog | Minutes to hours | 15-60 minutes | Changes infrequent, staleness acceptable |
| Static content | Days to indefinite | 24+ hours, version keying | Changes trigger deployments, not cache updates |
Consistency strategies:
1. Accept eventual consistency
For many use cases, eventual consistency is acceptable. Users understand that data might be slightly stale. Explicitly document staleness expectations.
2. Short TTLs
Use short TTLs (seconds to minutes) to bound maximum staleness. Trade-off: more cache misses, higher backend load.
3. Invalidation on write
Actively invalidate cache entries when data changes. Trade-off: complexity, potential for invalidation bugs, race conditions.
4. Read-around for critical paths
Bypass cache for specific reads where consistency is critical (e.g., reading your own recent update). Trade-off: more backend load, needs careful implementation.
5. Versioning
Include version numbers in cache keys. When data changes, bump version. Old cache entries become orphaned (cleaned up by TTL). Trade-off: more storage, version management overhead.
Explicitly document the consistency guarantees your cached system provides. 'Product prices may be up to 15 minutes stale' is a valid design decision when documented. Problems arise when consistency expectations are implicit and violated unexpectedly.
Complexity and Development Costs
Caching adds complexity to your system at multiple levels—code complexity, architectural complexity, and operational complexity. This complexity has real costs in development time, bugs, and cognitive load.
Code complexity:
- •Cache key generation — Every cacheable operation needs consistent key generation logic. Keys must be unique, deterministic, and account for all relevant parameters.
- •Cache population logic — Code to fetch-on-miss, serialize data, set TTLs, handle cache write failures gracefully.
- •Invalidation logic — Code to identify which cache entries need invalidation when data changes. Often scattered across multiple write paths.
- •Error handling — Cache failures shouldn't break the application. Need fallback logic, timeouts, circuit breakers.
- •Testing complexity — Tests need to cover cached paths, uncached paths, invalidation scenarios, and cache failure scenarios.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// WITHOUT CACHING - Simple, directasync function getProduct(id: string): Promise<Product> { return db.products.findUnique({ where: { id } });} // WITH CACHING - Significantly more complexasync function getProduct(id: string): Promise<Product> { const cacheKey = `product:${id}:v${SCHEMA_VERSION}`; try { // Try cache first const cached = await redis.get(cacheKey); if (cached) { metrics.cacheHit('product'); return JSON.parse(cached); } metrics.cacheMiss('product'); } catch (cacheError) { // Cache errors shouldn't break the request logger.warn('Cache read failed', { cacheKey, error: cacheError }); metrics.cacheError('product', 'read'); } // Fetch from database const product = await db.products.findUnique({ where: { id } }); if (product) { // Populate cache asynchronously (don't wait) redis.setex(cacheKey, PRODUCT_TTL_SECONDS, JSON.stringify(product)) .catch(err => { logger.warn('Cache write failed', { cacheKey, error: err }); metrics.cacheError('product', 'write'); }); } return product;} // ALSO NEED: Invalidation on every write pathasync function updateProduct(id: string, updates: ProductUpdate): Promise<Product> { const product = await db.products.update({ where: { id }, data: updates }); // Must remember to invalidate! const cacheKey = `product:${id}:v${SCHEMA_VERSION}`; await redis.del(cacheKey).catch(err => { logger.error('Critical: cache invalidation failed', { cacheKey, error: err }); // Now cache will serve stale data until TTL... }); return product;}Architectural complexity:
- New dependency — Cache becomes a critical infrastructure component. Its availability affects system availability.
- Data flow changes — Data no longer flows directly from source to consumer. Cache insertion points change the architecture.
- Cross-cutting concerns — Caching logic frequently cross-cuts multiple services and modules.
Cognitive load:
Developers working on cached systems must constantly think about:
- Is this data cached? What's the key?
- When I change this, what cache entries need invalidation?
- What's the consistency model? What staleness is acceptable?
- What happens if cache is unavailable?
This ongoing cognitive burden slows development and increases bug rates.
Every system has a complexity budget. Caching consumes part of that budget. If your system is already complex, adding caching may push it over the edge. Sometimes, simpler alternatives (database optimization, read replicas, connection pooling) achieve sufficient performance with less complexity.
Operational Burden
Beyond development complexity, caching adds operational burden—additional infrastructure to maintain, monitor, and troubleshoot. These ongoing costs are often underestimated when implementing caching.
Infrastructure management:
- •Provisioning and sizing — Cache needs appropriate memory allocation. Over-provision wastes money; under-provision causes eviction and poor hit rates.
- •High availability — Single cache node is a SPOF. Redis Sentinel, Redis Cluster, or Memcached pools for reliability.
- •Backups and persistence — Decide whether cache data needs persistence. Most don't, but some (session stores) do.
- •Scaling — As data grows, cache may need to scale. Horizontal scaling requires consistent hashing or cluster management.
- •Security — Cache contains application data. Network isolation, authentication, encryption in transit.
- •Upgrades and maintenance — Cache software updates, instance replacements, maintenance windows.
Monitoring requirements:
- Hit rate — Primary health indicator. Track overall and per-namespace.
- Memory usage — Approach to limits triggers eviction. Near-limit is a warning sign.
- Eviction rate — High evictions indicate undersizing.
- Connection count — Connection exhaustion causes failures.
- Latency — Cache should be fast. Rising latency indicates problems.
- Error rates — Connection errors, timeouts, other failures.
Incident response complexity:
When systems misbehave, caching adds investigation dimensions:
'Users are seeing stale data'
- Is it a cache TTL issue?
- Did invalidation fail?
- Which cache contains the stale data?
- Is it multiple cache layers?
'Requests are slow'
- Is it cache misses? What's the hit rate?
- Is the cache itself slow? Latency metrics?
- Is it thundering herd from cache failure?
'System is unstable'
- Is cache failing? What's its health?
- Did cache restart cause cold-start traffic surge?
- Are cache nodes being evicted/rotated?
Caching adds failure modes that generate pages. Cache node failures, memory exhaustion, high eviction rates, stale data incidents—all can trigger alerts. The on-call engineer must understand not just the application but the caching layer too. This isn't free.
Memory and Resource Costs
Caching requires memory, and memory costs money. While caching often reduces overall infrastructure costs, the cache itself isn't free. Understanding these costs helps with proper budgeting and sizing.
Memory cost factors:
-
Cache capacity — The amount of data you want to cache. Driven by working set size and desired hit rate.
-
Overhead — Memory managers, metadata, fragmentation. Actual consumption is typically 1.5-2x the data size.
-
Replication — HA configurations may duplicate data across nodes.
-
Serialization — Serialized representations may be larger than in-memory objects.
Cloud pricing reality:
| Node Type | Memory | Hourly Cost | Monthly Cost | Cost per GB |
|---|---|---|---|---|
| cache.t3.micro | 0.5 GB | $0.017 | ~$12 | $24/GB |
| cache.t3.small | 1.4 GB | $0.034 | ~$25 | $18/GB |
| cache.r6g.large | 13 GB | $0.126 | ~$92 | $7/GB |
| cache.r6g.xlarge | 26 GB | $0.252 | ~$184 | $7/GB |
| cache.r6g.4xlarge | 105 GB | $1.008 | ~$735 | $7/GB |
Working set estimation:
To estimate required cache size:
Required Cache Size = Working Set Size × (1 + Overhead Factor)
Where:
- Working Set Size = Number of Active Keys × Average Value Size
- Overhead Factor = ~0.5-1.0 (depends on key size, fragmentation)
Example calculation:
Scenario: E-commerce product cache
- Active products: 100,000
- Average product JSON size: 2 KB
- Average key size: 50 bytes
Raw data: 100,000 × 2 KB = 200 MB
With overhead (1.5x): 300 MB
With HA replication: 600 MB
Recommended: 1 GB cache node
Opportunity cost:
Memory spent on caching can't be used elsewhere:
- Application heap (larger in-process caches mean less memory for app)
- Database buffer pools (dedicated memory, not system RAM)
- Other services on shared infrastructure
Allocate cache memory deliberately, not by default.
Don't over-provision cache on day one. Start with conservative sizing, monitor hit rates and eviction patterns, and scale up based on actual data. Cache memory scales easily; wasted money doesn't come back.
When Caching is the Wrong Solution
Caching is not a universal solution. There are scenarios where caching creates more problems than it solves, or where other solutions are more appropriate.
Scenarios where caching doesn't help:
- •Write-heavy workloads — If writes dominate reads, cache is constantly invalidated. Little benefit, maximum complexity. Optimize writes directly instead.
- •Highly personalized data — If every request is unique (per-user, per-context), cache hit rate approaches zero. No shared benefit to caching.
- •Large working sets — If your entire dataset is frequently accessed, caching the whole thing is just replicating the database. May need a different architecture.
- •Strong consistency requirements — If stale data is never acceptable, caching adds risk without benefit. Use database features (read replicas) with consistency guarantees instead.
- •Cheap computation — If fetching/computing data is already fast, caching overhead may exceed benefit. Only cache expensive operations.
- •Unpredictable access patterns — Random access with no locality defeats caching. Hit rates stay low regardless of cache size.
Alternative solutions to consider:
| Problem | Caching Helps? | Alternative Solutions |
|---|---|---|
| Slow database queries | Sometimes | Query optimization, indexing, materialized views |
| Read scalability | Yes | Read replicas (simpler), caching (more complex but effective) |
| Write scalability | No | Sharding, async writes, queue-based processing |
| Network latency | Sometimes | Edge computing, regional deployments |
| Large data transfers | Sometimes | Compression, pagination, CDN for static assets |
| Connection limits | Yes | Connection pooling, PgBouncer/ProxySQL |
The premature optimization trap:
Caching is sometimes added prematurely—before there's evidence it's needed. This creates complexity without clear benefit.
Before adding caching, ask:
- What specific problem is caching solving?
- What's the current performance? What's the target?
- Have simpler solutions (indexing, query optimization) been tried?
- What's the read/write ratio? Is caching appropriate?
- What's the acceptable staleness? Can we achieve it?
- Do we have the operational maturity to manage cache infrastructure?
Don't cache by default. Cache when analysis shows it's the right solution. The best cache is often no cache at all—a system simple enough that caching isn't needed. Complexity has carrying costs that persist for the system's lifetime.
Making Informed Caching Decisions
Armed with understanding of both benefits and trade-offs, let's synthesize a framework for making informed caching decisions.
The caching decision framework:
- •Quantify the problem — Measure current latency, throughput, costs. Without baseline, you can't evaluate improvement.
- •Characterize the workload — Read/write ratio, access patterns, working set size, data freshness requirements.
- •Estimate cache potential — Based on workload, what hit rate is achievable? Use the models from earlier to predict impact.
- •Evaluate alternatives — Could query optimization, indexing, or architectural changes solve the problem more simply?
- •Assess operational readiness — Does the team have skills to operate cache infrastructure? Monitoring? Incident response?
- •Define staleness tolerance — Explicitly document what consistency model is acceptable. Get stakeholder agreement.
- •Plan for failure modes — What happens when cache fails? Cold start? Invalidation failure? Design the fallbacks.
- •Start small, measure, iterate — Don't implement comprehensive caching upfront. Start with highest-value, lowest-risk use cases.
The decision matrix:
Cross-referencing benefits against costs helps clarify the decision:
| Scenario | Likely Benefit | Likely Cost | Recommendation |
|---|---|---|---|
| High read/write ratio, infrequent changes | Very High | Low | Cache aggressively |
| Moderate read/write, tolerable staleness | High | Moderate | Cache strategically |
| Strong consistency required | Moderate | High | Cache carefully or avoid |
| Low read/write ratio | Low | Moderate | Avoid caching |
| Highly personalized data | Very Low | Moderate | Avoid caching |
| Trivial computation/latency | Low | Moderate | Avoid caching |
For each caching decision, document: What problem it solves, expected benefit (based on models), accepted trade-offs (specifically, staleness tolerance), invalidation strategy, fallback behavior. This documentation helps future maintainers understand the reasoning and constraints.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.