Marcio Cunha

How to use Redis as a distributed cache in Node.js APIs

Learn how to implement Redis in Node.js applications to store frequent data, reduce the load on your primary database, and speed up API response times.

Marcio Cunha•5 min
Also available in:PortuguêsEspañol
Summary
  • Redis acts as an in-memory database that speeds up the retrieval of repetitive data in your application
  • Expiration-based invalidation strategies prevent the system from serving outdated information to users
  • Caching frequent requests protects the relational database against unexpected traffic spikes
  • The ioredis library simplifies connection management and asynchronous command handling in Node.js
  • Proper handling of connection errors ensures the API keeps running even if the cache fails

Why your Node.js API needs an in-memory cache

When a web application grows, the database is usually the first component to suffer from slowdowns. Every user click triggers complex queries that consume processing power from the hard drive and main memory. In practice, this means identical requests keep recalculating the exact same results over and over again, wasting precious computational resources.

To solve this bottleneck, we introduce the concept of distributed caching. Instead of querying the relational database every time someone requests public information or a user profile, the application checks an ultra-fast intermediary layer. This approach is similar to keeping best-selling items right at the front window of a store, preventing clerks from walking all the way to the back warehouse for every single sale.

Redis stands out in this scenario because it stores all information directly in RAM, guaranteeing response times measured in milliseconds. Furthermore, as an independent tool running in the background, it can be shared across multiple instances of your Node.js API, keeping data synchronized in a scalable environment. If you want to deepen your knowledge about traffic flow and validate HTTP headers during development, check out the guide on how to use curl to test APIs and HTTP headers effectively.

Setting up the Redis connection in Node.js projects

The first practical step in implementation involves installing an official client or a reliable library compatible with the asynchronous Node.js ecosystem. In our case, we will use the ioredis library, known for its stability and native support for advanced commands. To begin, add the dependency to your project using your environment's standard package manager.

After installation, we need to initialize the connection by pointing to the network address where the Redis server is running. In practice, we create a centralized configuration file that exports a ready-to-use instance across the entire application. This prevents duplicate connections and ensures unified control over the cache client's lifecycle.

const Redis = require('ioredis');

const redisClient = new Redis({
  host: '127.0.0.1',
  port: 6379,
  retryStrategy: (times) => {
    const delay = Math.min(times * 50, 2000);
    return delay;
  }
});

redisClient.on('error', (err) => {
  console.error('Redis connection error:', err);
});

module.exports = redisClient;

Implementing the caching strategy on frequent routes

With the connection established, we can modify our API routes to check the cache before querying the main database. This logic follows a straightforward pattern: the application receives the request, generates a unique key based on received parameters, and checks if that key already exists in Redis. If it exists, we return the stored data immediately.

If the key is not found — an event known in computing as a cache miss —, we proceed with the traditional database query. Right after obtaining the raw result, we save a copy of this response in Redis accompanied by an expiration time. Thus, subsequent identical requests will be instantly served by the cache without overloading the primary system. Similarly to memory optimization, ensuring your relational database has proper structures is vital, as explained in our guide on how to create PostgreSQL indexes to speed up queries.

const express = require('express');
const redisClient = require('./redisClient');
const db = require('./database');

const app = express();

app.get('/api/products', async (req, res) => {
  const cacheKey = 'products:all';

  try {
    const cachedData = await redisClient.get(cacheKey);

    if (cachedData) {
      return res.json({
        source: 'cache',
        data: JSON.parse(cachedData)
      });
    }

    const products = await db.query('SELECT * FROM products');
    
    // Expires in 60 seconds to prevent stale data
    await redisClient.set(cacheKey, JSON.stringify(products), 'EX', 60);

    return res.json({
      source: 'database',
      data: products
    });
  } catch (error) {
    console.error('Failed to process request:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

Managing cache invalidation and data update strategies

The greatest challenge when working with distributed caching is not saving the information, but deciding the exact moment to delete or update it. When a user updates a product in the system, the old version stored in Redis remains active, creating visual inconsistencies. In practice, this means we must implement invalidation routines whenever a write operation occurs.

There are two primary approaches to solve this problem: time-based expiration (TTL) and event-driven invalidation. TTL works as a passive safety net, making data expire automatically after a few minutes. Event-driven invalidation, on the other hand, explicitly removes the cache key right after a successful insert, update, or delete in the database.

By combining these two techniques, we ensure the application remains performant without sacrificing the accuracy of information shown to clients. It is important to remember that cache should be treated as volatile and disposable; if the Redis server goes down unexpectedly, the API must be capable of falling back directly to the database without breaking the end-user experience. To explore these architectural concepts further in production, see our related technical analysis on How to test zener diodes on circuit board with multimeter.

Final thoughts on resilience and best practices

Using Redis as a distributed cache in Node.js APIs represents a turning point in modern system scalability. Combining fast responses with a drastic reduction in complex database queries allows infrastructure to support high traffic peaks with stability. However, the success of this architecture depends directly on conscious choices regarding data lifetime and a robust error-handling strategy.

Always remember to monitor the memory consumption of your cache server and set key eviction policies when maximum capacity is reached. With a clean implementation, constant monitoring, and efficient invalidation routines, your application will be prepared to grow sustainably and efficiently.