Cache Warming: Preloading Cache Before Traffic Arrives
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
- What problem does cache warming solve that lazy Caching does not?
- Why is it important to warm only hot keys rather than all keys?
- What is the trade-off between sequential and parallel warming?
- How does cache warming help prevent the thundering herd problem on database restart?
- 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
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