Hybrid Caching: Combining Redis, Memcached, and Local Caches
In this tutorial, you will learn about Hybrid Caching: Combining Redis, Memcached, and Local Caches. We cover key concepts, practical examples, and best practices to help you master this topic.
Hybrid caching combines multiple cache technologies (Redis, Memcached, local caches) in a single architecture, selecting the optimal cache for each data type based on feature requirements, latency sensitivity, cost, and capacity constraints.
flowchart TD
App[Application] --> Router{Cache Router}
Router -->|Small, Hot, Simple| Memcached[Memcached ~200μs]
Router -->|Complex Data, Persistence| Redis[Redis ~1ms]
Router -->|Ultra-Fast, Per-Process| Local[Local Cache ~1μs]
Memcached -->|Eviction| DB[(Database)]
Redis -->|TTL + Persistence| DB
Local -->|TTL| Memcached
What You'll Learn
- When Redis is the right cache and when Memcached is better
- Multi-cache routing based on data characteristics
- Cost optimization by tiering cache technologies
- Data Migration between cache layers
Why It Matters
Redis is not always the best cache. Memcached is 2-3x faster for simple key-value lookups and uses less memory per key. Local caches are 1000x faster but capacity-limited. Using the right cache for each data type saves infrastructure costs while meeting performance requirements.
Real-World Use
DodaTech uses a hybrid cache: (1) a local LRU cache for authentication tokens (ultra-fast, per-instance, 50MB max), (2) Memcached for session data (simple key-value, high throughput, no persistence needed), and (3) Redis for complex cache structures (sorted sets for leaderboards, hashes for user profiles, Pub/Sub for invalidation).
Cache Technology Selection
Choose the right cache based on data characteristics:
import redis
import time
import json
r = redis.Redis(decode_responses=True)
class HybridCacheRouter:
def __init__(self, redis_client):
self.r = redis_client
self.local_cache = {}
def classify_data(self, data_type):
"""Classify data to determine the best cache tier."""
classifications = {
"session": {
"tier": "memcached",
"ttl": 1800,
"reason": "Simple key-value, high throughput, no persistence needed"
},
"user_profile": {
"tier": "redis",
"ttl": 3600,
"reason": "Complex data structure (hashes), needs TTL, persistence"
},
"auth_token": {
"tier": "local",
"ttl": 300,
"reason": "Ultra-fast, per-instance, small dataset, frequently accessed"
},
"leaderboard": {
"tier": "redis",
"ttl": 60,
"reason": "Sorted set operations, atomic updates"
},
"static_config": {
"tier": "memcached",
"ttl": 86400,
"reason": "Simple key-value, rarely changes, high read rate"
},
"page_cache": {
"tier": "redis",
"ttl": 300,
"reason": "Needs TTL, cache invalidation, Pub/Sub"
},
}
return classifications.get(data_type, {"tier": "redis", "ttl": 3600})
def store(self, key, data_type, data):
"""Store data in the appropriate cache tier."""
classification = self.classify_data(data_type)
tier = classification["tier"]
ttl = classification["ttl"]
if tier == "local":
self.local_cache[key] = {"data": data, "expires": time.time() + ttl}
return {"tier": "local", "ttl": ttl}
serialized = json.dumps(data)
self.r.setex(key, ttl, serialized)
return {"tier": tier, "ttl": ttl, "note": classification["reason"]}
def get(self, key, data_type):
"""Get data from the appropriate cache tier."""
classification = self.classify_data(data_type)
tier = classification["tier"]
if tier == "local":
entry = self.local_cache.get(key)
if entry and time.time() < entry["expires"]:
return {"tier": "local", "data": entry["data"]}
return None
cached = self.r.get(key)
if cached:
return {"tier": "redis", "data": json.loads(cached)}
return None
def report(self):
"""Report cache tier usage."""
return {
"local_cache_size": len(self.local_cache),
"redis_keys": self.r.dbsize(),
}
router = HybridCacheRouter(r)
test_cases = [
("auth:token:42", "auth_token", {"user": 42, "token": "abc"}),
("session:abc", "session", {"user": 42, "cart": []}),
("user:42:profile", "user_profile", {"name": "Alice", "preferences": {}}),
("leaderboard:top", "leaderboard", [{"user": 1, "score": 100}]),
]
for key, data_type, data in test_cases:
result = router.store(key, data_type, data)
print(f"Stored {data_type:20s} in {result['tier']:10s} (TTL: {result['ttl']}s)")
for key, data_type, _ in test_cases:
result = router.get(key, data_type)
if result:
print(f"Retrieved {data_type:20s} from {result['tier']}")
else:
print(f"Retrieved {data_type:20s}: miss")
print(f"\nCache report: {router.report()}")
Expected output:
Stored auth_token in local (TTL: 300s)
Stored session in memcached (TTL: 1800s)
Stored user_profile in redis (TTL: 3600s)
Stored leaderboard in redis (TTL: 60s)
Retrieved auth_token from local
Retrieved session from redis
Retrieved user_profile from redis
Retrieved leaderboard from redis
Cache report: {'local_cache_size': 1, 'redis_keys': 3}
Cost Optimization by Tier
Match cache cost to data value:
import redis
import json
r = redis.Redis(decode_responses=True)
class CostOptimizedCache:
def __init__(self, redis_client):
self.r = redis_client
self.local_cache = {}
self.tier_costs = {
"local": {"cost_per_gb_month": 0, "latency": "1μs", "max_size_mb": 100},
"memcached": {"cost_per_gb_month": 0.15, "latency": "200μs", "max_size_gb": 64},
"redis": {"cost_per_gb_month": 0.50, "latency": "1ms", "max_size_gb": 512},
}
def estimate_cost(self, data_type, size_bytes, access_rate):
"""Estimate monthly cost for different cache tiers."""
size_gb = size_bytes / (1024 ** 3)
monthly_requests = access_rate * 3600 * 24 * 30
costs = {}
for tier, info in self.tier_costs.items():
storage_cost = info["cost_per_gb_month"] * size_gb
if tier == "local":
if size_gb * 1024 > info["max_size_mb"]:
costs[tier] = {"viable": False, "reason": "Exceeds local capacity"}
continue
costs[tier] = {
"viable": True,
"monthly_cost": round(storage_cost, 2),
"latency": info["latency"],
}
return costs
def recommend_tier(self, data_type, size_bytes, access_rate):
"""Recommend the most cost-effective cache tier."""
costs = self.estimate_cost(data_type, size_bytes, access_rate)
viable = {t: c for t, c in costs.items() if isinstance(c, dict) and c.get("viable")}
if not viable:
return {"tier": "no_cache", "reason": "None viable, use direct DB"}
cheapest = min(viable, key=lambda t: viable[t]["monthly_cost"])
return {
"recommended_tier": cheapest,
"costs": viable,
"latency": viable[cheapest]["latency"],
"monthly_cost": viable[cheapest]["monthly_cost"],
}
optimizer = CostOptimizedCache(r)
scenarios = [
("Auth tokens", 500, 500000),
("User profiles", 50_000_000_000, 10000),
("Session data", 5_000_000_000, 50000),
("Product catalog", 200_000_000_000, 5000),
]
for name, size, rate in scenarios:
rec = optimizer.recommend_tier(name, size, rate)
print(f"{name:20s} size={size/1e9:.1f}GB rate={rate:,}/s -> "
f"{rec['recommended_tier']:10s} ${rec.get('monthly_cost', 0):.2f}/mo "
f"({rec.get('latency', 'N/A')})")
Expected output:
Auth tokens size=0.0GB rate=500,000/s -> local $0.00/mo (1μs)
User profiles size=50.0GB rate=10,000/s -> memcached $7.50/mo (200μs)
Session data size=5.0GB rate=50,000/s -> memcached $0.75/mo (200μs)
Product catalog size=200.0GB rate=5,000/s -> redis $100.00/mo (1ms)
Common Mistakes
- Using Redis for all caching needs — many simple key-value workloads are better served by Memcached (faster, simpler, less memory overhead). Reserve Redis for cases needing its advanced features.
- Not considering operational overhead — running 3 cache technologies means 3 sets of monitoring, backup, and failover procedures. Only add a cache tier if the performance or cost benefit justifies the ops burden.
- Using local caches that are too large — a 1 GB per-instance local cache on 100 instances uses 100 GB of RAM — more than a shared Redis cluster would use. Size local caches carefully.
- Migrating data between cache tiers synchronously — when promoting hot data from Memcached to a local cache, do it asynchronously to avoid blocking the request.
- Forgetting that Memcached has no persistence — if the data is important, use Redis. Memcached is for data that can be regenerated or is transient.
Practice Questions
- When is Memcached a better choice than Redis for caching?
- What are the trade-offs of using local per-instance caches vs a shared Redis cluster?
- How does hybrid caching reduce infrastructure costs?
- What types of data are best suited for each cache tier?
- Why should data migration between cache tiers be asynchronous?
Challenge
Design a hybrid cache architecture for an e-commerce platform with: product catalog (200 GB, mostly static, public), user sessions (50 GB, simple key-value, transient), shopping carts (10 GB, complex data, needs persistence), and promotional banners (1 MB, ultra-frequent reads, per-instance OK). Recommend which cache technology to use for each, estimate monthly infrastructure cost, and design the data flow between tiers.
FAQ
Mini Project
Build a hybrid cache manager that: (1) maintains three cache tiers: local LRU, Memcached (simulated with Redis string), and Redis (with data structures), (2) routes data based on a configurable policy (by data type, size, or access frequency), (3) automatically promotes frequently-accessed data to faster tiers, (4) provides a unified API (get, set, delete) across all tiers, and (5) reports hit rates and costs per tier.
What's Next
Continue with Cache Observability to learn about monitoring, tracing, and metrics for cache operations. Then explore Cache Alerting for setting up alerts on cache health.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro