Introduction
Choosing the right TTL (Time-To-Live) is crucial. Too short wastes cache benefits; too long serves stale data. A well-tuned TTL strategy balances freshness with performance.
Key Concepts
- TTL (Time-To-Live): The duration a cached entry remains valid before Redis automatically deletes it.
- Jitter: Random variation added to TTLs to prevent simultaneous expiration of many keys.
- Soft Expiry: A secondary threshold before the actual TTL where background refresh is triggered.
Real World Context
An e-commerce site caching product prices needs short TTLs (minutes) to reflect frequent changes, while its category list can use long TTLs (hours) since categories rarely change. Getting these wrong means either stale prices or unnecessary database load.
Deep Dive
Factors Affecting TTL Choice
- Data volatility: How often does the source data change?
- Staleness tolerance: How much delay is acceptable?
- Cost of cache miss: How expensive is regenerating data?
- Memory constraints: Shorter TTLs = smaller cache footprint
TTL Strategies by Data Type
Static Content (Long TTL)
redis# Rarely changing data: 1 hour to 24 hours SET cache:countries "[...json...]" EX 86400 # 24 hours SET cache:categories "[...]" EX 3600 # 1 hour SET cache:config "[...]" EX 86400 # 24 hours
Semi-Static Content (Medium TTL)
redis# Changes occasionally: 5-60 minutes SET cache:product:123 "{...}" EX 900 # 15 minutes SET cache:user:profile:1001 "{...}" EX 1800 # 30 minutes SET cache:exchange_rates "{...}" EX 300 # 5 minutes
Dynamic Content (Short TTL)
redis# Changes frequently: seconds to minutes SET cache:stock:price:AAPL "150.25" EX 30 # 30 seconds SET cache:weather:NYC "{...}" EX 60 # 1 minute SET cache:trending "[...]" EX 120 # 2 minutes
Session Data (Activity-Based TTL)
redis# Sliding expiration - resets on activity HSET session:abc123 user_id 1001 EXPIRE session:abc123 1800 # 30 minutes # On each request: EXPIRE session:abc123 1800 # Reset TTL
Jittering TTLs to Prevent Stampedes
When many cache entries expire at the same time, you get a "thundering herd":
┌─────────────────────────────────────────────────────────────┐
│ Bad: All caches expire at T=300 │
│ │
│ T=300.001: Request 1 → MISS → Query DB │
│ T=300.002: Request 2 → MISS → Query DB │
│ T=300.003: Request 3 → MISS → Query DB │
│ ...100 simultaneous database queries! │
└─────────────────────────────────────────────────────────────┘
Solution: Add random jitter to TTLs:
redis# Instead of: SET cache:product:1 "..." EX 300 SET cache:product:2 "..." EX 300 SET cache:product:3 "..." EX 300 # Use jittered TTLs (270-330 seconds): SET cache:product:1 "..." EX 285 # 300 - random(0,30) SET cache:product:2 "..." EX 312 # 300 + random(0,30) SET cache:product:3 "..." EX 298 # 300 - random(0,30)
Implementation:
pythonimport random def jittered_ttl(base_ttl, jitter_percent=10): jitter = base_ttl * jitter_percent / 100 return int(base_ttl + random.uniform(-jitter, jitter)) # Usage ttl = jittered_ttl(300) # Returns 270-330
Early Expiration (Probabilistic)
Refresh cache before it expires to avoid misses:
┌─────────────────────────────────────────────────────────────┐
│ Cache TTL: 300 seconds │
│ │
│ Soft expiry: 270 seconds (90%) │
│ Hard expiry: 300 seconds │
│ │
│ If time > soft_expiry: │
│ - Return cached data │
│ - Asynchronously refresh cache │
└─────────────────────────────────────────────────────────────┘
Store expiration time in the value:
redisSET cache:data '{"value":"...","expires_at":1704067200}' EX 310 # On read, check expires_at # If close to expiration, trigger background refresh
Common Pitfalls
- Using the same TTL for all data — Static configuration data and real-time stock prices need wildly different TTLs. Categorize data by volatility.
- Forgetting TTL jitter — Without jitter, batch-cached data expires simultaneously, causing a thundering herd of database queries.
Best Practices
- Add 10% jitter to all TTLs — This simple technique prevents cache stampedes with minimal code.
- Use sliding TTLs for sessions — Reset the TTL on each access so active sessions stay alive while idle ones expire.
Summary
- Match TTL to data volatility: seconds for dynamic, hours for static
- Add jitter to prevent thundering herd on simultaneous expiration
- Use early/soft expiry to refresh cache before TTL hits zero
- Sliding TTLs work well for session data that resets on activity
Code Examples
python
import random
def jittered_ttl(base_ttl, jitter_percent=10):
"""Add random jitter to prevent thundering herd"""
jitter = base_ttl * jitter_percent / 100
return int(base_ttl + random.uniform(-jitter, jitter))
# Usage: TTLs spread between 270-330 seconds
ttl = jittered_ttl(300) # e.g., 287, 312, 298...
r.set("cache:product:1", data, ex=ttl)