Skip to content

Hybrid Caching: Combining Redis, Memcached, and Local Caches

DodaTech Updated 2026-06-28 7 min read

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

  1. When is Memcached a better choice than Redis for caching?
  2. What are the trade-offs of using local per-instance caches vs a shared Redis cluster?
  3. How does hybrid caching reduce infrastructure costs?
  4. What types of data are best suited for each cache tier?
  5. 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

What is hybrid caching?

Hybrid caching uses multiple cache technologies in one architecture, selecting the best cache for each data type. Common combinations: local in-memory for ultra-hot data, Memcached for simple high-throughput data, and Redis for complex data requiring persistence.

When should I use Memcached instead of Redis?

Use Memcached when you need simple key-value caching, high throughput (multi-threaded), and data loss on restart is acceptable. Use Redis when you need data structures, persistence, replication, Pub/Sub, or Lua scripting.

What are the downsides of hybrid caching?

Operational complexity — multiple cache technologies to maintain, monitor, and back up. Code complexity — routing logic to select the right cache tier. Data consistency — ensuring writes to one tier propagate correctly.

How do I decide data placement across cache tiers?

Place data based on: access frequency (hot -> local, warm -> Memcached, cool -> Redis), size (small -> local), persistence needs (important -> Redis), and latency requirements (ultra-fast -> local, fast -> Memcached, standard -> Redis).

Can I migrate data between cache tiers automatically?

Yes. Implement a promotion policy: when data is accessed more than N times per minute, promote it from Memcached to a local cache. When access drops below M, demote it back. This optimizes for the access pattern over time.

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