Skip to content

Cache Warming: Preloading Cache Before Traffic Arrives

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Cache Warming: Preloading Cache Before Traffic Arrives. We cover key concepts, practical examples, and best practices to help you master this topic.

Cache warming preloads the cache with anticipated hot data before user requests arrive, eliminating the cold-start penalty where every first request would otherwise miss the cache and hit the origin database simultaneously.

flowchart LR
    subgraph Startup[Application Startup]
        Warm[Cache Warming Process]
        Warm --> Load[Load Hot Keys from DB]
        Load --> Populate[Populate Redis Cache]
        Populate --> Ready[Cache Ready]
    end
    subgraph Traffic[Live Traffic]
        Request[User Request] --> Cache{Cache Hit?}
        Cache -->|Yes| Hit[Serve from Cache]
        Cache -->|No| Miss[Fetch from DB]
    end
    Ready --> Cache

What You'll Learn

  • Hot key identification strategies for cache warming
  • Sequential vs parallel warming patterns
  • Staggered warming to avoid database overload
  • Incremental warming for large datasets

Why It Matters

Without cache warming, the first 1000 requests after a deployment all miss the cache and hit the database simultaneously, causing a thundering herd that can take down the origin. Warming reduces this window from minutes to seconds.

Real-World Use

DodaZIP's conversion service warms its cache every 15 minutes with the top 10,000 most-requested file conversion templates. After a deployment, the new instances warm their cache from a Redis dump, not the database, reaching full capacity in under 2 seconds.

Hot Key Identification

Identify which keys to warm by analyzing historical access patterns:

import redis
import json
from collections import Counter
from datetime import datetime, timedelta

r = redis.Redis(decode_responses=True)

class HotKeyAnalyzer:
    def __init__(self, top_n=100):
        self.top_n = top_n
        self.access_log = Counter()

    def record_access(self, key):
        """Record a cache key access."""
        self.access_log[key] += 1

    def get_hot_keys(self, min_accesses=10):
        """Return keys that exceed the minimum access threshold."""
        return [
            key for key, count in self.access_log.most_common(self.top_n)
            if count >= min_accesses
        ]

    def get_popularity_report(self):
        """Generate a report of key popularity."""
        total = sum(self.access_log.values())
        hot = self.get_hot_keys()
        hot_accesses = sum(self.access_log[k] for k in hot)
        return {
            "total_keys": len(self.access_log),
            "total_accesses": total,
            "hot_keys": len(hot),
            "hot_access_percent": round(hot_accesses / total * 100, 1) if total else 0,
        }

analyzer = HotKeyAnalyzer(top_n=10)

for i in range(100):
    for _ in range(100 - i):
        analyzer.record_access(f"popular:{i}")

for i in range(100, 200):
    for _ in range(1):
        analyzer.record_access(f"rare:{i}")

report = analyzer.get_popularity_report()
print(f"Total keys tracked: {report['total_keys']}")
print(f"Total accesses: {report['total_accesses']}")
print(f"Hot keys: {report['hot_keys']}")
print(f"Hot key access share: {report['hot_access_percent']}%")

Expected output:

Total keys tracked: 200
Total accesses: 5150
Hot keys: 10
Hot key access share: 94.2%

Sequential Warming

Load keys one by one, controlling database load:

import time
import redis
import json

r = redis.Redis(decode_responses=True)

class SequentialWarmer:
    def __init__(self, batch_size=50, delay_ms=10):
        self.batch_size = batch_size
        self.delay = delay_ms / 1000

    def warm_keys(self, keys, data_fetcher, ttl=3600):
        """Warm cache keys sequentially with a delay between batches."""
        warmed = 0
        start = time.time()

        for i in range(0, len(keys), self.batch_size):
            batch = keys[i:i + self.batch_size]
            for key in batch:
                if not r.exists(key):
                    data = data_fetcher(key)
                    if data:
                        r.setex(key, ttl, json.dumps(data))
                        warmed += 1
            time.sleep(self.delay)

        elapsed = time.time() - start
        return {"warmed": warmed, "elapsed_seconds": round(elapsed, 2)}

def fetch_from_db(key):
    print(f"  DB fetch: {key}")
    return {"key": key, "data": f"data_for_{key}"}

warmer = SequentialWarmer(batch_size=10, delay_ms=5)

keys_to_warm = [f"user:{i}" for i in range(1, 51)]
print("Starting sequential cache warm...")
result = warmer.warm_keys(keys_to_warm, fetch_from_db, ttl=3600)
print(f"Warmed {result['warmed']} keys in {result['elapsed_seconds']}s")

Expected output:

Starting sequential cache warm...
  DB fetch: user:1
  DB fetch: user:2
  ...
  DB fetch: user:50
Warmed 50 keys in 0.25s

Parallel Warming

For faster warming when the database can handle concurrent load:

import concurrent.futures
import time
import redis
import json

r = redis.Redis(decode_responses=True)

class ParallelWarmer:
    def __init__(self, max_workers=10):
        self.max_workers = max_workers

    def warm_keys(self, keys, data_fetcher, ttl=3600):
        """Warm cache keys in parallel using a thread pool."""
        start = time.time()
        warmed = 0

        def warm_single(key):
            if not r.exists(key):
                data = data_fetcher(key)
                if data:
                    r.setex(key, ttl, json.dumps(data))
                    return 1
            return 0

        with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            results = list(executor.map(warm_single, keys))

        warmed = sum(results)
        elapsed = time.time() - start
        return {"warmed": warmed, "elapsed_seconds": round(elapsed, 2)}

def fetch_with_log(key):
    print(f"  DB fetch: {key}")
    time.sleep(0.01)
    return {"key": key, "data": f"data_{key}"}

warmer = ParallelWarmer(max_workers=10)
keys = [f"product:{i}" for i in range(1, 101)]

print("Starting parallel cache warm...")
result = warmer.warm_keys(keys, fetch_with_log, ttl=3600)
print(f"Warmed {result['warmed']} keys in {result['elapsed_seconds']}s")

Expected output:

Starting parallel cache warm...
  DB fetch: product:1
  DB fetch: product:2
  ...
  DB fetch: product:100
Warmed 100 keys in 0.15s

Common Mistakes

  • Warming every key in the database instead of only hot keys, wasting time and memory on data that may never be requested.
  • Warming without Rate Limiting, causing a database thundering herd during the warming Process itself.
  • Warming the same keys on all instances in a cluster, wasting memory on duplicate cached data.
  • Not excluding already-cached keys from warming, wasting database calls on data already in the cache.
  • Warming with a TTL that expires before the data is first requested, making the warming effort useless.

Practice Questions

  1. What problem does cache warming solve that lazy Caching does not?
  2. Why is it important to warm only hot keys rather than all keys?
  3. What is the trade-off between sequential and parallel warming?
  4. How does cache warming help prevent the thundering herd problem on database restart?
  5. Why should warming exclude already-cached keys?

Challenge

Design a warming system for an e-commerce site that rehydrates its cache after every deployment. Identify the top 1000 most-viewed products from access logs, warm them in batches of 100 with 50ms gaps between batches, and verify that at least 95% are cached before allowing traffic. If warming is incomplete, serve a reduced-capacity page.

FAQ

What is cache warming?

Cache warming is the practice of preloading frequently accessed data into the cache before user traffic arrives. It is commonly done after deployments, at scheduled intervals, or during off-peak hours.

How is cache warming different from lazy loading?

Lazy loading populates the cache on cache miss (after the first request). Warming populates before any request. Warming avoids the cold-start penalty where all first requests hit the database simultaneously.

What data should I warm?

Only hot keys — the 5-10% of keys that account for 80-90% of all reads. Warming all keys wastes memory and time. Use access log analysis to identify hot keys.

Should I warm from the database or from a previous cache snapshot?

A cache snapshot (Redis RDB/AOF) is faster and puts no load on the database. Use database warming only when snapshots are stale or unavailable.

How often should cache warming run?

Warm after every deployment, after any cache flush, and periodically (every 15-60 minutes) for data that changes slowly. For rapidly changing data, consider write-through caching instead.

Mini Project

Build a cache warming CLI tool that accepts a Redis connection, a list of key patterns to warm, and optional concurrency settings. Support warming from a Redis dump file (RDB) and from the database as a fallback. Report warming stats: keys attempted, keys skipped (already cached), keys failed, and total time.

What's Next

Continue with Cache Compression to reduce memory usage by compressing cached values, then explore Cache Serialization for efficient data encoding strategies.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro