Imagine you have an API like:
GET /api/products
At first, everything looks fine.
But then your application starts getting more traffic.
Maybe 100,000 requests arrive in a day. And for every request, your API does the same thing:
Client → API → Database → Response
The database keeps fetching the same product data over and over again.
Now ask yourself:
Does the database really need to do that work every time?
If the data hasn't changed, probably not.
Instead, we can store frequently requested data somewhere faster and reuse it for subsequent requests.
That's the basic idea behind caching.
And one of the most popular tools used to implement caching is Redis.
What Is Caching?
Caching means temporarily storing data that your application is likely to need again.
Instead of asking the database for the same data every time:
Client
↓
API
↓
Database
↓
Response
we introduce a cache:
Client
↓
API
↓
Cache
↓
Response
If the cache doesn't have the data, the application falls back to the database:
┌─────────┐
│ Client │
└────┬────┘
↓
┌─────────┐
│ API │
└────┬────┘
↓
┌─────────┐
│ Cache │
└────┬────┘
│
Cache Miss
↓
┌─────────┐
│ Database│
└─────────┘
The important idea is simple:
Don't repeatedly perform expensive work if you can safely reuse the result.
A Simple Example
Suppose your API frequently receives:
GET /api/products/42
The first request might look like this:
API
↓
Cache
↓
Not Found
↓
Database
↓
Store result in Cache
↓
Return response
The next request can look like:
API
↓
Cache
↓
Return response
The database doesn't have to execute the same query again.
This is where caching starts making a real difference.
Cache Hit vs Cache Miss
When working with caching, you'll constantly hear two terms:
cache hit and cache miss.
Cache Hit
A cache hit means the requested data already exists in the cache.
For example:
GET /api/products/42
API → Redis → HIT → Response
The application finds the data immediately and can return it without querying the database.
Cache Miss
A cache miss means the requested data isn't currently in the cache.
GET /api/products/42
API → Redis → MISS
↓
Database
↓
Redis
↓
Response
The application has to retrieve the data from the database and can then store it in Redis for future requests.
A cache miss isn't an error.
It simply means:
"The cache doesn't have what I need, so I'll get it from somewhere else."
A useful metric here is the cache hit rate.
For example, if 90 out of every 100 requests are served from the cache, your cache hit rate is 90%.
A higher hit rate generally means fewer requests are reaching your database.
Where Does Redis Come In?
Redis is an in-memory data store commonly used for caching.
Instead of relying primarily on disk-based storage like a traditional database, Redis keeps data in memory, which allows applications to access it very quickly.
A simple cached value might look like:
Key: product:42
Value:
{
"id": 42,
"name": "Keyboard",
"price": 2499
}
When the API needs product 42, it can ask Redis for:
product:42
If Redis has the value, the application can use it.
If Redis doesn't have it, the application queries the database.
The important distinction is:
Database → Source of truth
Redis → Fast temporary copy
Redis isn't necessarily replacing your database.
It's usually sitting in front of it to reduce unnecessary database work.
A Real-World Example
Let's say an e-commerce application has a "Trending Products" endpoint.
During a busy period, 50,000 users request the same data.
Without caching:
50,000 API requests
↓
50,000 database queries
But suppose the cache has a 90% hit rate.
Now the traffic might look like:
50,000 API requests
↓
45,000 → Redis
5,000 → Database
That's a huge difference.
The database didn't become faster.
It simply had much less work to do.
And that's one of the biggest reasons caching is useful in system design.
TTL: The Cache Can't Live Forever
Caching introduces an obvious problem:
What happens when the original data changes?
Suppose Redis contains:
product:42
price: ₹2499
Someone then updates the product:
Database
price: ₹2999
But Redis still contains:
price: ₹2499
Now your API could return the wrong price.
This is called stale data.
One common way to deal with this is TTL — Time To Live.
TTL tells Redis how long a cached value should remain available.
For example:
product:42
TTL: 10 minutes
After 10 minutes, the entry expires.
The next request becomes a cache miss:
API
↓
Redis → MISS
↓
Database
↓
Redis
↓
Response
The application gets the latest value and caches it again.
The TTL Tradeoff
TTL creates a simple tradeoff:
Short TTL
↓
Fresher data
↓
More database requests
Long TTL
↓
Better cache efficiency
↓
Higher chance of stale data
There isn't one perfect TTL.
It depends on the data.
For example:
- Frequently changing data → shorter TTL
- Data that changes occasionally → longer TTL
- Data that rarely changes → potentially much longer TTL
The important question is:
How stale can this data safely be?
Cache Invalidation: The Tricky Part
TTL isn't the only way to deal with stale data.
Consider a user profile.
Redis contains:
user:123
name: "John"
The user changes their name to:
Alex
The database is updated:
Database
user:123
name: "Alex"
But Redis still has:
user:123
name: "John"
Now you have two different versions of the same data.
This is where cache invalidation becomes important.
Option 1: Delete the Cache
After updating the database:
Update Database
↓
Delete Redis Key
The next request won't find the cached value.
So it fetches the latest version from the database and stores it again.
This is often a simple approach.
Option 2: Update the Cache
Another approach is:
Update Database
↓
Update Redis
Now both contain the latest value.
This can avoid a cache miss, but your application now has to keep both systems synchronized correctly.
Option 3: Let TTL Handle It
You can also simply allow the cached value to expire.
It's easy to implement, but stale data may remain available until the TTL expires.
So cache invalidation isn't just a Redis problem.
It's a data consistency decision.
You need to decide:
How much stale data can the application tolerate?
The Cache-Aside Pattern
One of the most common caching strategies is called cache-aside.
The application controls the entire process.
The flow looks like this:
Request
↓
Check Cache
↓
┌─────┴─────┐
│ │
HIT MISS
│ │
↓ ↓
Return Query Database
Data ↓
Store
in Cache
↓
Return Data
The application doesn't blindly trust the cache.
It checks the cache first and falls back to the database when necessary.
This pattern is simple and works well for many read-heavy applications.
A Simple Caching Architecture
Putting everything together:
┌──────────┐
│ Client │
└────┬─────┘
↓
┌──────────┐
│ API │
└────┬─────┘
↓
┌──────────┐
│ Redis │
└────┬─────┘
│
┌─────┴─────┐
│ │
HIT MISS
│ │
↓ ↓
Response Database
│
↓
Redis
│
↓
Response
The request flow is straightforward:
- Client sends a request.
- API checks Redis.
- If the data exists, return it.
- If it doesn't, query the database.
- Store the result in Redis.
- Return the response.
The next request can hopefully be served directly from Redis.
Implementing It with Node.js and Redis
The actual caching logic can be surprisingly small.
For example:
app.get("/products/:id", async (req, res) => {
const key = `product:${req.params.id}`;
// Check cache
const cachedProduct = await redis.get(key);
if (cachedProduct) {
return res.json(JSON.parse(cachedProduct));
}
// Cache miss - query database
const product = await getProductFromDatabase(req.params.id);
// Store in Redis for 10 minutes
await redis.set(
key,
JSON.stringify(product),
{ EX: 600 }
);
return res.json(product);
});
The important part isn't the Redis syntax.
It's the decision-making:
Check cache
↓
Found?
┌──Yes──→ Return cached data
│
No
↓
Query database
↓
Store result in cache
↓
Return result
That's the cache-aside pattern in practice.
What Happens If Redis Goes Down?
Here's something easy to overlook.
Adding Redis means you've added another system to your architecture.
So now you have another possible failure.
What happens if Redis becomes temporarily unavailable?
Your application shouldn't necessarily stop working completely.
Depending on the application, a possible fallback is:
API
↓
Redis
↓
Unavailable
↓
Database
↓
Response
You may experience slower responses, but the application can continue serving requests.
This leads to an important system-design principle:
A cache should usually improve your system, not become the only thing keeping it alive.
The exact failure strategy depends on the application and how critical the cached data is.
Other Problems Caching Can Introduce
Caching can solve performance problems, but it also introduces new ones.
Stale Data
The cache may contain an older version of the data.
Cache Invalidation
You need a strategy for deciding when cached data should be removed or updated.
Memory Usage
Caches store data in memory, so storing too much can become expensive.
Cache Failures
Redis itself can become unavailable or experience problems.
Cache Stampede
Imagine a popular cached item expires.
Thousands of requests arrive at almost the same time.
They all see:
Cache MISS
and all try to query the database.
Instead of reducing database load, the cache expiration can suddenly create a large burst of database traffic.
This is one of the problems that becomes more important as systems scale.
Operational Complexity
Now your team also needs to think about:
- Memory usage
- Expiration
- Invalidation
- Failures
- Monitoring
- Hit rates
- Eviction policies
So caching isn't free.
You're trading some additional complexity for better performance and lower database load.
When Should You NOT Use Caching?
Caching sounds useful until you start putting it everywhere.
You shouldn't automatically cache every database query.
Caching might not be useful when:
- The data changes constantly.
- Almost every request requires the newest version.
- The endpoint has a very low cache hit rate.
- The data is rarely requested.
- The cached object is extremely large.
- The performance improvement isn't worth the added complexity.
For example, if an endpoint is requested only a few times per day, adding Redis might not provide much value.
Before introducing a cache, ask:
What problem am I actually trying to solve?
If the database is already fast enough, adding another system may simply make the architecture more complicated.
How Do You Decide What to Cache?
A simple mental model is:
Is this data requested frequently?
↓
Is fetching it expensive?
↓
Can the application tolerate some staleness?
↓
Can we define an expiration or
invalidation strategy?
↓
Yes
↓
Consider caching
A few practical rules can help.
Cache Frequently Requested Data
If the same data is requested thousands of times, caching can significantly reduce repeated work.
Measure the Cache Hit Rate
If almost every request is a cache miss, the cache may not be helping much.
Have a Freshness Strategy
Know how long the data can remain stale and decide whether to use TTL, invalidation, or another approach.
Don't Cache Everything
More caching means more complexity.
Cache the data that actually benefits from it.
Think About Failure
Ask what happens if Redis becomes unavailable.
A good architecture should have a reasonable fallback when possible.
The Bigger System Design Lesson
It's easy to think about caching as simply:
"Redis is fast, so let's put Redis in front of the database."
But that's not really the important part.
The important questions are:
- What data is requested repeatedly?
- How expensive is it to fetch?
- How often does it change?
- How stale can it be?
- What should happen when it expires?
- What happens if the cache fails?
- Is the added complexity worth the performance improvement?
Those are system-design questions. Redis is simply one tool that can help you implement the answer.
Conclusion
Caching isn't about making every request magically faster.It's about avoiding work that doesn't need to happen again.
If thousands of users are requesting the same piece of data, sending every request to the database may be unnecessary.
A cache can handle many of those requests and leave the database to focus on work that actually needs to be done.
Redis makes this pattern practical with fast in-memory access and features such as TTL.
But caching also introduces its own problems:
- Stale data
- Cache invalidation
- Memory usage
- Cache failures
- Cache stampedes
- Additional operational complexity
That's the real system-design lesson.
As an application grows, scaling isn't always about making the database handle more requests.
Sometimes the better solution is to make sure the database doesn't receive unnecessary requests in the first place.
Good caching isn't about storing everything.
It's about knowing what doesn't need to be fetched again.
Top comments (0)