Redis Caching Fundamentals: Patterns, Pitfalls, and Interview Concepts
Redis caching in Node.js: cache-aside, TTLs, stampede protection and eviction policies, plus the system design interview questions that come up about them.
This article is published in English.
Sources: Redis Isn’t Just a Cache: The Concepts Every Backend Developer Should Know
Why do we need a cache when we already have a database?
Picture an endpoint in your application that looks like this:
GET /products/123
A client hits that route, and your server turns around and queries MongoDB:
API → MongoDB → Product
The database hands back the product record, and everything works fine.
Now picture 10,000 people asking for that same product at roughly the same time.
You'd end up repeating the exact same round trip thousands of times over:
Request
↓
Database
↓
Same Product
Your database keeps redoing identical work for identical questions.
Wouldn't it be better if the answer could just be remembered somewhere?
That's essentially what caching is for.
What Is Caching?
A cache is a quicker storage layer that holds onto frequently requested data for a short while, so your app isn't forced to hit the slower primary data store every single time.
Instead of the usual flow:
Request → Database → Response
we introduce a shortcut:
Request
↓
Cache
↓
Hit? ── Yes → Response
│
No
↓
Database
↓
Cache
↓
Response
The point isn't to get rid of the database entirely.
It's to cut down on database work that doesn't need to happen twice.
Cache Hit vs Cache Miss
These two concepts come up constantly, so it's worth knowing them cold.
Cache Hit
This happens when the data you're looking for is already sitting in the cache.
Request
↓
Redis
↓
Found ✅
↓
Response
Cache Miss
This happens when the cache has nothing for you.
Request
↓
Redis
↓
Not Found ❌
↓
Database
↓
Store in Redis
↓
Response
A handy metric here is the cache hit ratio:
Cache Hit Ratio =
Cache Hits / Total Cache Requests
A larger ratio typically signals that more traffic is being answered directly from the cache instead of the underlying database.
That said, a bigger number doesn't automatically mean success. If the cache is full of outdated or low-value entries, a high hit ratio doesn't make it worthwhile.
So Where Does Redis Come In?
Redis is an in-memory data store frequently used for caching, though its capabilities go well beyond that single use case.
It offers a range of data structures, including:
- Strings
- Hashes
- Lists
- Sets
- Sorted sets
- Streams
- JSON
- Plus other specialized types
For a straightforward cache, you can think of it like this:
Key Value
────────────────────────────
product:123 Product data
user:42 User data
Since Redis holds hot data in memory, reads served from the cache can be remarkably quick.
Redis in a Node.js Application
Using the node-redis client, you install it with:
npm install redis
Setting up a basic connection takes just a couple of lines:
import { createClient } from "redis";
const redis = createClient();
await redis.connect();
From there, storing and retrieving values is simple:
await redis.set("user:42", "Hamza");
const user = await redis.get("user:42");
The official Redis documentation currently points to node-redis as the recommended client for Node.js projects.
Knowing SET and GET, though, only gets you so far.
The real question worth answering is:
How do you actually design an application around Redis?
The Most Important Pattern: Cache-Aside
If there's one pattern every backend developer should learn first, it's Cache-Aside.
In this pattern, the application itself is responsible for managing the cache.
Request
↓
Check Redis
↓
┌───────────────┐
│ Cache Hit? │
└───────────────┘
↓ Yes ↓ No
Response Database
↓
Redis
↓
Response
A stripped-down version in code looks like this:
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);const product = await Product.findById(id);await redis.set(key, JSON.stringify(product), {
EX: 300
});
The EX: 300 option sets a TTL of 300 seconds on the cached entry.
Cache-aside is arguably the most widely used caching pattern with Redis.
TTL: The Cache Shouldn’t Live Forever
Say you've cached the following:
product:123
What should happen if the product's price is updated afterward?
Without an expiration, users could keep seeing the old price indefinitely.
That's the reason for TTL, or Time To Live.
Product stored
↓
TTL = 300 seconds
↓
Expires
↓
Next request → Database → Fresh cache
TTL puts a limit on how long stale data can possibly be served.
Even so, resist the urge to pick an unnecessarily long TTL.
The correct value depends entirely on how much staleness your application can afford.
Cache Invalidation: The Famous Hard Problem
Consider this scenario:
Database:
price = ₹799
Redis:
price = ₹799
Then the database gets updated:
Database:
price = ₹899
But Redis is still holding onto:
price = ₹799
At this point your application is serving inconsistent data depending on where it reads from.
A common fix is to clear the cache whenever the underlying data changes:
await Product.findByIdAndUpdate(id, update);
await redis.del(`product:${id}`);
After that, the next request follows this path:
Redis → Miss
↓
Database → ₹899
↓
Redis → Store fresh value
For systems built around cache-aside, simply deleting the affected cache entry on every write tends to be easier and safer than trying to keep the cache perpetually in sync with the database.
Three Cache Problems You Should Know
These make for great interview discussion topics.
1. Cache Stampede
Picture a heavily used key expiring:
product:123 → EXPIRED
Right after that, 1,000 requests come in at once.
Every single one of them misses the cache.
Every single one of them falls through to the database.
1000 requests
↓
1000 DB queries 💥
The cache was meant to shield the database from load, but the expiration event itself just triggered a spike.
Typical ways to guard against this include:
- Locking or single-flight requests, so only one request repopulates the cache while others wait
- Staggering TTLs so keys don't all expire together
- Probabilistic early refresh, which renews a key slightly before it actually expires
- Prewarming frequently requested data ahead of time
Redis's own documentation explicitly covers stampede protection strategies for hot keys.
2. Cache Penetration
What happens when clients keep asking for data that simply doesn't exist?
GET /users/999999
Redis returns a miss.
The database returns "not found."
The next identical request repeats the exact same cycle.
A malicious actor or a broken client could exploit this to hammer the database with pointless lookups.
A useful countermeasure is negative caching — caching the fact that something is missing:
user:999999 → NOT_FOUND
TTL → short
With this in place, repeated lookups for the same nonexistent record can be answered directly from Redis instead of hitting the database every time.
3. Cache Avalanche
Now imagine a large batch of cached keys all expiring at roughly the same moment.
Key A → expired
Key B → expired
Key C → expired
Key D → expired
↓
Massive DB traffic
This situation is known as a cache avalanche.
A typical way to reduce the risk is to avoid assigning identical expiration times to large groups of keys — for instance, by adding a bit of random jitter to each TTL.
Cache Eviction: What Happens When Redis Runs Out of Memory?
Redis memory isn't infinite.
Once memory usage hits the configured ceiling, Redis starts removing keys according to whichever eviction rule you've configured. Redis ships with a handful of these eviction modes, letting you evict least-recently-used entries across the whole keyspace or only among keys with an expiration set, evict least-frequently-used entries in either of those same two scopes, evict based on which keys are closest to expiring, or refuse new writes entirely once memory is full.
LRU essentially means:
Get rid of whatever hasn't been touched in a while.
LFU essentially means:
Get rid of whatever isn't accessed very often.
Which one fits best depends on your specific access patterns.
Redis Isn’t Just a Cache
This point comes up often in interviews.
Because Redis offers several built-in data structures, it can serve purposes well beyond caching:
Strings → Caching
Hashes → Objects / sessions
Lists → Queues
Sets → Unique collections
Sorted Sets → Leaderboards
Streams → Event processing
So the claim that:
"Redis is a caching database"
only tells part of the story.
A more accurate description is:
Redis is an in-memory data structure store, typically used for caching, but also capable of handling sessions, queues, leaderboards, rate limiting, pub/sub messaging, and more.
Redis's official documentation on data types walks through these structures along with other specialized types it supports.
What About Redis Persistence?
Since Redis lives in memory, does a restart wipe everything out?
Not necessarily.
Redis offers several persistence strategies:
- RDB — periodic point-in-time snapshots of the dataset
- AOF — a log that records every write operation
- RDB + AOF combined
- No persistence at all
If Redis is only acting as a cache, losing its contents on restart is often fine, since the application can simply repopulate it from the primary database.
But if Redis is holding data that actually matters on its own, persistence settings become a much bigger concern.
A Common Interview Trap
“Why not just store everything in Redis?”
Because Redis's memory is a limited and comparatively costly resource next to conventional persistent storage.
That means you shouldn't cache indiscriminately.
Good candidates for caching share these traits:
- They're requested often
- They're costly to fetch or compute
- Some staleness is tolerable
- They're worth the memory cost of keeping them close at hand
Treat Redis as a performance-boosting layer sitting alongside your system, not as a wholesale substitute for your main database.
Local Cache vs Distributed Cache
Imagine a Node.js application running across three separate servers:
Server A → Cache A
Server B → Cache B
Server C → Cache C
Each of these servers ends up maintaining its own independent cache.
A user might land on Server A and get a cache hit, then later land on Server B and get a miss for the exact same data.
Introducing a shared Redis instance changes the picture:
┌→ Server A ─┐
Client ──────┼→ Server B ─┼→ Redis
└→ Server C ─┘
Now every application instance draws from the same shared cache.
This shared-state benefit is a big reason Redis shines particularly well once an application scales out horizontally.
The Advanced Interview Picture
Suppose an interviewer poses this challenge:
"Design a caching strategy for a high-traffic product API."
A weak answer stops at:
"I'll use Redis."
A stronger answer walks through the reasoning:
Request
↓
Redis
↙ ↘
Hit Miss
↓ ↓
Response Database
↓
Redis
↓
Response
Then it covers each of these design decisions:
1. Cache pattern
Go with cache-aside.
2. TTL
Set it according to how much staleness the product data can tolerate.
3. Invalidation
Delete the corresponding key whenever the underlying data is updated.
4. Stampede protection
Apply locking, request coalescing, or early refresh techniques.
5. Eviction
Pick a suitable LRU or LFU policy for the workload.
6. Key design
product:{id}
user:{id}
7. Monitoring
Keep an eye on:
- Hit ratio
- Miss ratio
- Latency
- Memory usage
- Eviction counts
- Hot keys
- Redis errors
That kind of layered reasoning makes for a far more convincing system-design answer than simply asserting that "Redis is fast."
Rapid-Fire Interview Review
Why bother caching at all?
It cuts down latency and spares your primary data store from handling the same expensive reads over and over.
Why pick Redis specifically?
It offers fast in-memory access, expiration support, versatile data structures, and features built for distributed systems.
What does cache-aside mean?
The application looks in the cache first; on a miss it queries the database and then writes that result back into the cache.
What does TTL mean?
It's the lifespan a cached entry gets before it automatically expires.
What does cache invalidation mean?
It means clearing or refreshing cached entries whenever the source data changes.
What is a cache stampede?
A situation where a large number of requests miss the cache at once and all hammer the database simultaneously.
LRU versus LFU?
LRU evicts whatever was used least recently. LFU evicts whatever has been used least often overall.
Does every query deserve caching?
Not at all. Base the decision on how often data is accessed, how expensive it is to fetch, how much staleness is acceptable, and how much memory you can spare.
The Mental Model Worth Keeping
Skip memorizing Redis commands as a starting point. Instead, hold onto the underlying architecture:
┌─────────────┐
Request ─────→│ Redis │
└──────┬──────┘
│
Cache Hit?
↙ ↘
YES NO
↓ ↓
Response Database
↓
Redis
↓
Response
Then run through four questions:
What belongs in the cache?
How long should it stay there?
When does it need to be invalidated?
What happens if the cache fails or expires under heavy load?
Working through these questions is what separates surface-level caching knowledge from production-grade thinking.
Wrapping Up
Caching is not just:
"Stick Redis in front of MongoDB."
A solid caching strategy is fundamentally about trade-offs.
You're exchanging:
Memory for speed
and, at times:
Freshness for performance
Redis supplies the mechanisms, but it's your application logic that determines how they get used.
If you're gearing up for interviews, don't settle for saying:
"Redis is an in-memory database."
Be prepared to walk through:
Cache-aside, TTL, invalidation, hit ratio, eviction, stampede, penetration, avalanche, distributed caching, and consistency.
Once you see how these concepts fit together, you've moved past just knowing Redis.
You now understand how high-traffic systems shield their databases while staying fast.