DEV Community

Cover image for Caching & Redis: How to Reduce Database Load and Make APIs Faster
Tanu Priya
Tanu Priya

Posted on

Caching & Redis: How to Reduce Database Load and Make APIs Faster

Imagine you have an API like:

GET /api/products
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

we introduce a cache:

Client
  ↓
 API
  ↓
Cache
  ↓
Response
Enter fullscreen mode Exit fullscreen mode

If the cache doesn't have the data, the application falls back to the database:

             ┌─────────┐
             │  Client │
             └────┬────┘
                  ↓
             ┌─────────┐
             │   API   │
             └────┬────┘
                  ↓
             ┌─────────┐
             │  Cache  │
             └────┬────┘
                  │
              Cache Miss
                  ↓
             ┌─────────┐
             │ Database│
             └─────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The first request might look like this:

API
 ↓
Cache
 ↓
Not Found
 ↓
Database
 ↓
Store result in Cache
 ↓
Return response
Enter fullscreen mode Exit fullscreen mode

The next request can look like:

API
 ↓
Cache
 ↓
Return response
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

When the API needs product 42, it can ask Redis for:

product:42
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Someone then updates the product:

Database
price: ₹2999
Enter fullscreen mode Exit fullscreen mode

But Redis still contains:

price: ₹2499
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

After 10 minutes, the entry expires.

The next request becomes a cache miss:

API
 ↓
Redis → MISS
 ↓
Database
 ↓
Redis
 ↓
Response
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

The user changes their name to:

Alex
Enter fullscreen mode Exit fullscreen mode

The database is updated:

Database
user:123
name: "Alex"
Enter fullscreen mode Exit fullscreen mode

But Redis still has:

user:123
name: "John"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The request flow is straightforward:

  1. Client sends a request.
  2. API checks Redis.
  3. If the data exists, return it.
  4. If it doesn't, query the database.
  5. Store the result in Redis.
  6. 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);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)