Redis Caching: Cache-Aside, TTL & Eviction

Lab
Easy
Medium
Hard

Cache-aside basicsEasy

Check Redis, fall back to the database, store the result.

The Redis cache

Capacity (keys that fit in memory)5
Eviction policy (maxmemory-policy)
TTL on every key

Traffic pattern

1 / 32hit ratio 0%
Cache miss
GET B → miss. B isn't in Redis. The app queries the database, then stores the result in Redis (cache-aside).

Cache-aside in action

request 1 of 32 · t = 0s
App
GET B
Redis
miss → query
Database
Redis memory (1/5 slots)policy LRU
B
used 1× · last t=0s
empty
empty
empty
empty
stored B
Request stream
So far
Cache hits0
Cache misses1
Database reads1
Evictions0
What if? Hit ratio by policy
LRU
53%
LFU
63%
FIFO
47%
RANDOM
50%
NOEVICTION
69%
✓ 53% hit ratio over the whole run — 15 database reads, 10 evictions

More memory and a policy that matches your access pattern are the two levers that move the hit ratio.

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.

PatternHow writes workTrade-off
Cache-asideWrite the DB, invalidate the keySimple; brief staleness; first read is a miss
Write-throughWrite cache and DB togetherCache always fresh; slower writes, caches unused data
Write-behindWrite cache, flush to DB laterFast writes; risk of data loss on crash
Read-throughThe cache layer loads from the DB itselfSimpler 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:

PolicyEvictsBest when
allkeys-lruThe least recently used keyGeneral caching; recent use predicts reuse
allkeys-lfuThe least frequently used keyStable hot set; protects against one-off scans
allkeys-random / FIFO-likeA random (or oldest) keyUniform access, or when overhead must be minimal
volatile-*Only keys that have a TTLMix of cache keys and permanent data
noevictionNothing — writes fail with an OOM errorRedis 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.

Keep exploring

More interactive labs

All labs

Redis Commands & Data Structures

Strings, lists, hashes, sets and sorted sets, one command at a time.

Redis15 min · Beginner

What Happens When 100 Reads Hit the Database

Connection pools, CPU workers, caches and replicas, as 100 requests arrive at once.

Databases12 min · Intermediate

JS Event Loop Visualizer

See the call stack, microtasks and timers run, frame by frame.

JavaScript10 min · Intermediate

CORS & Preflight Simulator

Edit the request and the server headers, then see exactly why the browser blocks it.

Networking12 min · Intermediate

HTTP Caching Explained

Cache-Control, ETag and 304s, one request at a time.

Networking12 min · Intermediate

What Happens When You Type a URL

DNS, TCP, TLS and HTTP — with a stopwatch.

Networking10 min · Beginner

How Database Indexes Work (B-Tree)

See a B-tree lookup beat a full table scan, and why some queries ignore your index.

Databases12 min · Intermediate

SQL Isolation Levels Lab

Run two transactions side by side and watch dirty reads, phantoms and write skew appear.

Databases15 min · Advanced

Sharding & Replication

Split data across servers, copy it for reads, and see what each choice costs.

Databases15 min · Advanced

Webhooks: Delivery, Retries & Signatures

Why you get duplicate deliveries, lost events and forged requests — and how to stop them.

Queues & Jobs12 min · Intermediate

Cron Jobs: Schedules, Overlaps & Missed Runs

Write a cron expression and watch what really happens when jobs overlap or the scheduler is down.

Queues & Jobs10 min · Beginner

Job Queues with BullMQ (Redis)

Watch jobs move through waiting, active, delayed, completed and failed — and see what BullMQ stores in Redis.

Queues & Jobs14 min · Intermediate

RabbitMQ: Exchanges, Queues, Acks & Dead Letters

Direct, topic and fanout routing, competing consumers, prefetch, redelivery and dead-letter queues, drawn.

Queues & Jobs14 min · Intermediate

Git Commands, HEAD & Refs (Visual)

Every core git command drawn with arrows: three areas, commits, branches, HEAD and remotes.

Git15 min · Beginner

Git Merge vs Rebase

Watch the commit graph change under merge, rebase and squash.

Git10 min · Beginner