Expiration & Caching Patterns

Redis
Chapter 5 · Expiration & Caching Patterns

⏱️ Expiration & Caching Patterns

node3-5 mentioned "Redis patterns" for caching without ever teaching them. This chapter is that deep dive: how keys expire on their own, the cache-aside pattern nearly every Redis-backed cache uses, and the well-documented failure mode — cache stampede — that catches teams who skip past it.

TTL & EXPIRE

Any key can be given a time to live — after which Redis deletes it automatically, with no manual cleanup code required.

127.0.0.1:6379> SET session:abc123 "user-104" EX 3600 OK 127.0.0.1:6379> TTL session:abc123 (integer) 3598 127.0.0.1:6379> PERSIST session:abc123 (integer) 1

SET ... EX 3600 sets the value and a 3600-second (1 hour) expiration in one command. TTL reports the remaining seconds. PERSIST removes the expiration entirely, making the key permanent again. Once a key's TTL reaches zero, Redis removes it on its own — no cron job, no scheduled cleanup.

The Cache-Aside Pattern

Cache-aside is the standard shape nearly every Redis cache follows: check Redis first; on a hit, return immediately; on a miss, query the real database, then store the result in Redis with a TTL so the next request hits the cache instead.

async function getProduct(id) { const cached = await redis.get(`product:${id}`); if (cached) return JSON.parse(cached); // cache hit const product = await db.query('SELECT * FROM products WHERE id = ?', [id]); // cache miss await redis.set(`product:${id}`, JSON.stringify(product), { EX: 300 }); return product; }

The database is only ever queried on a genuine miss — every subsequent call within the 300-second TTL is served entirely from Redis, at a fraction of the latency.

Cache Stampede Prevention

A popular key's TTL eventually expires — and if that key is being requested constantly, many concurrent requests can all miss the cache at the exact same moment, each one independently querying the database to repopulate it. This is a cache stampede (also called "thundering herd" or "dog-piling") — a real, well-documented production incident pattern where a single expiring cache key causes a sudden spike of duplicate load against the database.

No Stampede Protection vs. a Simple Lock

No Protection

100 concurrent requests all miss the cache at once → all 100 hit the database simultaneously to fetch the same thing.

Lock-Based Protection

Only the first request acquires a lock and queries the database; the other 99 wait briefly and then read the now-repopulated cache.

A Simple Lock Using SET NX

SET key value NX only succeeds if the key doesn't already exist — a lightweight mutex:

async function getProductSafely(id) { const cached = await redis.get(`product:${id}`); if (cached) return JSON.parse(cached); const gotLock = await redis.set(`lock:product:${id}`, "1", { NX: true, EX: 5 }); if (!gotLock) { // someone else is already repopulating — wait briefly, then retry the cache await sleep(50); return getProductSafely(id); } const product = await db.query('SELECT * FROM products WHERE id = ?', [id]); await redis.set(`product:${id}`, JSON.stringify(product), { EX: 300 }); return product; }

Only the request that successfully sets the lock key queries the database; every other concurrent request backs off and retries the cache shortly after, rather than all piling onto the database at once. Chapter 10's Lua-scripted approach makes this pattern fully atomic.

CommandPurpose
EXPIRE key secondsSets a TTL on an existing key
SET key value EX secondsSets a value and a TTL in one command
TTL keyReports remaining seconds before expiration
PERSIST keyRemoves a key's expiration
SET key value NXOnly sets if the key doesn't exist — the basis of a simple lock
Complementary technique: jittering TTLs

If many keys are all set with the exact same TTL at roughly the same time (e.g. a bulk cache-warming job), they tend to expire together — recreating stampede conditions across many keys at once rather than just one. Adding a small random amount ("jitter") to each key's TTL — e.g. 300 seconds plus a random 0–30 seconds — spreads expirations out over time, reducing the odds of many keys going stale simultaneously. This is a lightweight complement to locking, not a replacement for it.

💻 Coding Challenges

Challenge 1: Set a Session With Expiration

Write the redis-cli command to store a session token for user 55 that expires after 30 minutes, then the command to check how much time is left.

Goal: Practice the SET ... EX shorthand and TTL together.

→ Solution

Challenge 2: Trace a Cache-Aside Flow

Walk through what happens, step by step, for two back-to-back calls to getProduct(42) using this chapter's cache-aside example — assume the cache starts empty.

Goal: Practice tracing the hit/miss branches of the cache-aside pattern across multiple calls.

→ Solution

Challenge 3: Diagnose a Stampede

A popular product page's cache key expires, and the database sees a sudden 200-query spike within the same second. Diagnose what likely happened and propose a fix using this chapter's material.

Goal: Practice recognizing a cache stampede from its symptoms and applying the lock-based fix.

→ Solution

🎯 What's Next

The next chapter is Pub/Sub & Scaling Real-Time AppsPUBLISH/SUBSCRIBE from first principles, deepening the WebSockets course's brief mention of a Redis pub/sub adapter (web-sockets1-7).