Caching & Redis

Keep frequently used data close and fast. Learn caching strategies, invalidation, eviction policies, the classic pitfalls, and what Redis can do beyond caching.

Intermediate⏱ 6 min readLesson 6 of 12#backend#caching#redis#performance

The big idea

You keep the things you use every day (keys, phone, wallet) on your desk, not in the basement storage room. Going to the basement takes 5 minutes; reaching to your desk takes 1 second.

A cache is your desk: a small, fast store of copies of data that is expensive to fetch or compute.

The memory hierarchy: the closer the data, the faster it isThe memory hierarchy: the closer the data, the faster it is

Where data livesTypical latencyAnalogy
CPU cache~1 nsIn your hand
RAM / in-process cache~100 nsOn your desk
Redis over the network~0.5 msIn the next room
Database with an index~5 msDownstairs
Slow DB query / external API100 ms – 2 sAcross town

Where caches live

Drawing diagram…

Every layer can answer the request and save all the layers behind it.

Strategy 1: Cache-aside (lazy loading) ⭐ most common

The app checks the cache first; on a miss it reads the database and stores the result.

Drawing diagram…
async function getProduct(id) {
  const key = `product:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const product = await db.products.findById(id);
  if (product) await redis.set(key, JSON.stringify(product), { EX: 300 }); // TTL 5 min
  return product;
}

Other strategies

StrategyHow it worksProsCons
Cache-asideApp reads cache → DB on miss → fills cacheSimple, only caches what's usedFirst request is slow; can serve stale data
Read-throughThe cache itself loads from the DB on a missCleaner app codeNeeds a cache library/provider that supports it
Write-throughWrite to cache and DB togetherCache is always freshSlower writes, caches data nobody reads
Write-behindWrite to cache, flush to DB laterVery fast writesRisk of losing data if the cache crashes
Drawing diagram…

The hardest part: invalidation

📘 "There are only two hard things in computer science: cache invalidation and naming things." (Phil Karlton)

When data changes, the cached copy is stale. Options:

  1. TTL (time to live): entries expire automatically. Simple; data can be stale for up to the TTL.
  2. Delete on write: when the product changes, delete its cache key so the next read reloads it.
  3. Events: publish product.updated; every service with a cached copy removes it.
async function updateProduct(id, changes) {
  await db.products.update(id, changes);
  await redis.del(`product:${id}`); // delete, don't update: avoids race conditions
}

💡 Prefer deleting the key over writing the new value into it. Two concurrent updates can otherwise leave the older value in the cache.

Eviction: when the cache is full

Memory is limited, so something must go:

PolicyEvictsGood for
LRU (Least Recently Used)The item not used for the longest timeMost workloads ✅
LFU (Least Frequently Used)The item used the fewest timesStable "popular items"
FIFOThe oldest itemSimple streams
TTLExpired itemsTime-sensitive data

In Redis: maxmemory 2gb and maxmemory-policy allkeys-lru.

Classic caching problems

1. Cache stampede (thundering herd)

A popular key expires; 10,000 requests miss at the same moment and all hit the database.

Drawing diagram…

Fixes: a lock so only one request rebuilds the value (others wait or get the stale copy), refreshing hot keys before they expire, and adding random jitter to TTLs so keys don't all expire together.

2. Cache penetration

Requests for IDs that don't exist always miss and always hit the DB (sometimes an attack). Fix: cache the "not found" result briefly, or use a Bloom filter to reject IDs that certainly don't exist.

3. Hot key

One key (a celebrity's profile) gets so much traffic it overloads a single Redis node. Fix: a small in-process cache in front of Redis, or replicate the key.

Redis: more than a cache

Redis is an in-memory data-structure server. Its data types solve many real problems:

TypeCommandsUse case
StringSET, GET, INCR, EXPIRECache, counters, rate limits
HashHSET, HGETALLUser sessions, objects
ListLPUSH, RPOP, BLPOPSimple queues, recent activity
SetSADD, SISMEMBERUnique visitors, tags
Sorted setZADD, ZRANGE, ZREVRANKLeaderboards, priority queues, time windows
StreamXADD, XREADGROUPEvent log with consumer groups
Pub/SubPUBLISH, SUBSCRIBEReal-time notifications, chat
// Leaderboard in 3 commands
await redis.zAdd("leaderboard", { score: 4200, value: "ana" });
await redis.zIncrBy("leaderboard", 50, "bo");
const top10 = await redis.zRangeWithScores("leaderboard", 0, 9, { REV: true });

// Rate limit: max 100 requests per minute per user
const key = `rate:${userId}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60);
if (count > 100) throw new TooManyRequestsError();

Persistence and availability

  • RDB snapshots: save the whole dataset every N minutes (fast restarts, may lose the last minutes).
  • AOF (append-only file): log every write (safer, bigger files). You can use both.
  • Replication + Sentinel: automatic failover to a replica.
  • Redis Cluster: shards data across nodes using 16,384 hash slots.

⚠️ Redis keeps data in RAM. Treat it as a cache or as fast secondary storage, not as your only copy of critical data (unless you configure persistence carefully).

What to cache (and what not to)

✅ Cache: data read far more often than written (product pages, configs), expensive computations, sessions, rate-limit counters, 3rd-party API responses.

❌ Don't cache: data that must be perfectly fresh (account balance at the moment of payment), data that is rarely re-read, highly personalised data with no reuse.

Key takeaways

  • A cache stores copies of expensive data close to where it's needed.
  • Cache-aside is the default pattern: check cache → DB on miss → store with a TTL.
  • Invalidate by TTL, by deleting on write, or through events. Prefer delete over update.
  • Guard against stampedes (locks, jitter), penetration (cache "not found") and hot keys.
  • Redis offers strings, hashes, lists, sets, sorted sets and streams: leaderboards, rate limits, queues, sessions.