Introduction
Combine multiple cache layers for optimal performance and cost efficiency. Each tier has different speed and capacity characteristics.
Key Concepts
- Tier 1 (Local Memory): Application heap cache — fastest (<0.001ms) but smallest (MBs).
- Tier 2 (Redis): Network cache — fast (1-5ms) with medium capacity (GBs).
- Tier 3 (Database): Persistent storage — slowest (10-100ms) but largest (TBs).
- Cache Promotion: Copying data from a slower tier to a faster tier on access.
Real World Context
Large-scale applications like Netflix and Twitter use multi-tier caching. Local memory serves the hottest data with zero network cost, Redis handles the warm tier, and the database is the cold fallback. This architecture supports millions of requests per second.
Deep Dive
Cache Hierarchy
┌─────────────────────────────────────────────────────────────┐
│ Tier 1: Application Memory (fastest, smallest) │
│ Response time: <1ms, Size: MBs │
├─────────────────────────────────────────────────────────────┤
│ Tier 2: Redis (fast, medium) │
│ Response time: 1-5ms, Size: GBs │
├─────────────────────────────────────────────────────────────┤
│ Tier 3: Database (slow, largest) │
│ Response time: 10-100ms, Size: TBs │
└─────────────────────────────────────────────────────────────┘
Implementation Pattern
pythonclass MultiTierCache: def __init__(self, redis_client, local_cache_size=1000): self.redis = redis_client self.local = LRUCache(local_cache_size) def get(self, key, fetch_func, local_ttl=60, redis_ttl=300): # Tier 1: Check local cache data = self.local.get(key) if data is not None: return data # Tier 2: Check Redis data = self.redis.get(key) if data is not None: # Populate local cache self.local.set(key, data, ttl=local_ttl) return data # Tier 3: Fetch from database data = fetch_func() # Populate both caches self.redis.set(key, data, ex=redis_ttl) self.local.set(key, data, ttl=local_ttl) return data def invalidate(self, key): # Invalidate both tiers self.local.delete(key) self.redis.delete(key)
Hot vs Cold Data Separation
Keep hot data in memory, cold data in Redis:
pythonclass HotColdCache: def __init__(self, redis_client, hot_threshold=100): self.redis = redis_client self.hot_cache = {} # In-memory for hot keys self.access_count = {} # Track access frequency self.hot_threshold = hot_threshold def get(self, key, fetch_func): # Track access self.access_count[key] = self.access_count.get(key, 0) + 1 # Check if hot (in-memory) if key in self.hot_cache: return self.hot_cache[key] # Check Redis data = self.redis.get(key) if data is None: data = fetch_func() self.redis.set(key, data, ex=300) # Promote to hot cache if accessed frequently if self.access_count[key] >= self.hot_threshold: self.hot_cache[key] = data return data
CDN + Redis + Database
For web applications:
┌───────────────────────────────────────────────────────────┐
│ User Request │
│ ↓ │
│ CDN (static assets, edge-cached pages) │
│ ↓ (cache miss) │
│ Application Server │
│ ↓ │
│ Redis (dynamic data cache) │
│ ↓ (cache miss) │
│ Database │
└───────────────────────────────────────────────────────────┘
pythondef get_product_page(product_id): cache_key = f"page:product:{product_id}" # Check if CDN should cache (via headers) # Static products: Cache-Control: max-age=3600 # Dynamic products: Cache-Control: private # Redis for dynamic data product = cache.get( f"product:{product_id}", lambda: db.get_product(product_id), redis_ttl=300 ) return render_product_page(product)
Regional Cache Layers
For global applications:
┌───────────────────────────────────────────────────────────┐
│ US User → US Redis (local) → Global Redis → Database │
│ EU User → EU Redis (local) → Global Redis → Database │
│ Asia User → Asia Redis (local) → Global Redis → Database │
└───────────────────────────────────────────────────────────┘
Benefits:
- Lower latency for users in each region
- Reduced cross-region traffic
- Better fault tolerance
Common Pitfalls
- Inconsistent invalidation across tiers — Deleting a key in Redis but forgetting the local cache means stale data persists until the local TTL expires. Always invalidate all tiers.
- Local cache too large — An unbounded local cache competes with your application for heap memory, causing garbage collection pauses.
Best Practices
- Use short TTLs for Tier 1 — Local cache TTLs of 30-60 seconds limit staleness while still absorbing most read traffic.
- Monitor each tier independently — Track hit rates per tier to understand which layer absorbs the most load.
Summary
- Multi-tier caching checks fastest tier first, falling through to slower tiers on miss
- Local memory (Tier 1) + Redis (Tier 2) + Database (Tier 3) is the standard architecture
- Always invalidate all tiers when data changes to prevent staleness
- Short local TTLs (30-60s) balance freshness with hit rate
Code Examples
python
class MultiTierCache:
def __init__(self, redis_client, local_cache_size=1000):
self.redis = redis_client
self.local = LRUCache(local_cache_size)
def get(self, key, fetch_func, local_ttl=60, redis_ttl=300):
data = self.local.get(key) # Tier 1: <0.001ms
if data is not None:
return data
data = self.redis.get(key) # Tier 2: ~1ms
if data is not None:
self.local.set(key, data, ttl=local_ttl)
return data
data = fetch_func() # Tier 3: ~50ms
self.redis.set(key, data, ex=redis_ttl)
self.local.set(key, data, ttl=local_ttl)
return data