Learn the theory
Redis Caching: Cache-Aside, TTL & Eviction: how it works
4 min read · Updated
The most common use of Redis is as a cache in front of a slower database: read the value from memory if it's there (a hit), otherwise load it from the database and store it for next time (a miss). It's simple, but the details — what to evict when memory is full, how long entries live, and what happens when a hot key expires — decide whether the cache saves your database or quietly hurts it.
In short
- Cache-aside: check the cache, on a miss read the database, then write the result to the cache.
- The metric that matters is the hit ratio; every miss is a database read.
- When memory is full, an eviction policy (LRU, LFU, FIFO, random, noeviction) chooses what to drop.
- LRU drops what wasn't used recently; LFU drops what is used least often and survives one-off scans.
- A cache stampede happens when a hot key expires and many requests rebuild it at once — prevent it with locks (single-flight), jittered TTLs or early refresh.
The cache-aside pattern
async function getUser(id) {
const key = 'user:' + id;
const cached = await redis.get(key); // 1. look in Redis
if (cached) return JSON.parse(cached); // hit: ~1 ms
const user = await db.users.find(id); // 2. miss: ask the database
await redis.set(key, JSON.stringify(user), 'EX', 300); // 3. fill the cache
return user;
}The application owns the logic (hence “aside”). Reads populate the cache lazily, so only data that's actually requested occupies memory. Writes usually update the database and then delete the cache key so the next read reloads fresh data.
| Pattern | How writes work | Trade-off |
|---|---|---|
| Cache-aside | Write the DB, invalidate the key | Simple; brief staleness; first read is a miss |
| Write-through | Write cache and DB together | Cache always fresh; slower writes, caches unused data |
| Write-behind | Write cache, flush to DB later | Fast writes; risk of data loss on crash |
| Read-through | The cache layer loads from the DB itself | Simpler app code; needs a smart cache |
Hit ratio and capacity
hit ratio = hits ÷ (hits + misses). A 90% hit ratio means the database sees one request in ten. Because real traffic is skewed — a few keys get most requests — a cache much smaller than the dataset can still achieve a high ratio. Doubling memory yields diminishing returns once your hot set fits.
Eviction policies
When Redis reaches maxmemory, the maxmemory-policy setting decides what happens on the next write:
| Policy | Evicts | Best when |
|---|---|---|
allkeys-lru | The least recently used key | General caching; recent use predicts reuse |
allkeys-lfu | The least frequently used key | Stable hot set; protects against one-off scans |
allkeys-random / FIFO-like | A random (or oldest) key | Uniform access, or when overhead must be minimal |
volatile-* | Only keys that have a TTL | Mix of cache keys and permanent data |
noeviction | Nothing — writes fail with an OOM error | Redis as a primary store, not a cache |
TTLs: freshness vs hit ratio
A TTL bounds how stale cached data can be, but every expiry is a guaranteed miss. Short TTLs mean fresher data and more database load; long TTLs mean the opposite. Choose per data type: seconds for prices or inventory, minutes for profiles, hours for rarely changing reference data.
- Add jitter (random ±10%) so keys created together don't all expire together.
- Consider serving stale data briefly while refreshing in the background.
- Invalidate explicitly on writes when freshness matters.
Cache stampede (thundering herd)
When a very popular key expires, every concurrent request misses at the same instant and each one queries the database to rebuild it — turning one expiry into dozens or thousands of identical queries, which can overload the database. This is a cache stampede.
- Single-flight / mutex: only one request rebuilds the value (
SET lock:key token NX EX 10); the others wait briefly or serve stale data. - Early/probabilistic refresh: refresh a key shortly before it expires.
- Jittered TTLs to avoid synchronized expiry.
- Never expire the hottest keys; refresh them with a background job.
Other pitfalls
- Cache penetration: requests for keys that don't exist always miss. Cache a short-lived “not found” marker or use a Bloom filter.
- Stale data: race between a read-fill and a write-invalidate can cache old values; keep TTLs as a safety net.
- Big values and hot keys: a single huge or very hot key can overload one Redis node; split or replicate it.
- Cold start: an empty cache sends all traffic to the database; warm it before cutting over.
Frequently asked questions
- What is the cache-aside pattern?
- The application checks the cache first; on a miss it reads the database and stores the result in the cache. On writes it updates the database and invalidates the cached entry.
- What is the difference between LRU and LFU?
- LRU evicts the entry that hasn't been used for the longest time. LFU evicts the entry that has been used least often. LFU keeps popular items through one-off scans, while LRU adapts faster to changing access patterns.
- What is a cache stampede?
- A burst of identical database queries caused by many requests missing the same key at once — typically when a hot key expires. Locking so only one request rebuilds the value, jittered TTLs and early refresh prevent it.
- How do I choose a TTL?
- Balance freshness against hit ratio: use short TTLs for fast-changing or critical data and longer ones for stable data, add random jitter, and invalidate explicitly when a write must be visible immediately.
- What does Redis do when memory is full?
- It follows the configured maxmemory-policy: evict keys by LRU, LFU, random or TTL rules, or with noeviction reject writes with an out-of-memory error.
- What is a good cache hit ratio?
- It depends on the workload, but 90%+ is common for read-heavy data with a hot set. Track it, and look at the absolute number of database reads — not just the percentage.