Skip to content

18 Caching Redis

DodaTech 4 min read

title: Redis Caching in Node.js REST APIs — Complete Guide weight: 28 date: 2026-06-28 lastmod: 2026-06-28 description: Implement Redis caching in Node.js REST APIs for response caching, database query caching, session storage, and cache invalidation with TTL-based expiration. tags: [api-development, nodejs]


Redis caching in Node.js REST APIs stores frequently accessed data in memory, reducing database load and response times by caching API responses, database queries, and computed results with configurable TTL expiration.

```mermaid
flowchart TD
  A[Request] --> B{In Cache?}
  B -->|Yes| C[Return Cached Response]
  B -->|No| D[Query Database]
  D --> E[Store in Redis]
  E --> F[Return Response]
  C --> G[Fast Response <5ms]
  F --> H[Normal Response 50ms+]
  style A fill:#e1f5fe
  style B fill:#fff9c4
  style C fill:#c8e6c9
  style D fill:#f3e5f5

Redis stores key-value pairs with optional TTL. Cache API responses using the request URL as the key and the JSON response as the value. Set appropriate TTLs based on how often data changes. Invalidate cache entries when data is mutated.

Think of Redis like a whiteboard in a busy office. Frequently referenced information (phone extensions, meeting room status) is written on the whiteboard so you do not have to go to the filing cabinet (database) every time.

Example: Redis Client Setup

const redis = require('redis');

const redisClient = redis.createClient({
  url: process.env.REDIS_URL || 'redis://localhost:6379'
});

redisClient.on('error', (err) => console.error('Redis error:', err));
redisClient.on('connect', () => console.log('Connected to Redis'));

(async () => {
  await redisClient.connect();
})();

module.exports = redisClient;

Example: Caching Middleware

const redisClient = require('./redis');

const cacheMiddleware = (ttl = 60) => {
  return async (req, res, next) => {
    // Only cache GET requests
    if (req.method !== 'GET') {
      return next();
    }

    const key = `cache:${req.originalUrl}`;

    try {
      const cached = await redisClient.get(key);

      if (cached) {
        console.log(`Cache hit: ${key}`);
        return res.json(JSON.parse(cached));
      }

      console.log(`Cache miss: ${key}`);

      // Override res.json to cache the response
      const originalJson = res.json.bind(res);
      res.json = (body) => {
        redisClient.setEx(key, ttl, JSON.stringify(body));
        return originalJson(body);
      };

      next();
    } catch (error) {
      console.error('Cache error:', error);
      next();
    }
  };
};

// Usage
app.get('/api/products', cacheMiddleware(300), productController.list);

Example: Cache Invalidation on Mutations

const redisClient = require('./redis');

const invalidateCache = (...patterns) => {
  return async (req, res, next) => {
    // Store the original json method
    const originalJson = res.json.bind(res);

    res.json = async (body) => {
      // Invalidate cache after successful mutations
      if (res.statusCode >= 200 && res.statusCode < 300) {
        for (const pattern of patterns) {
          const keys = await redisClient.keys(pattern);
          if (keys.length > 0) {
            await redisClient.del(keys);
            console.log(`Invalidated ${keys.length} cache entries for pattern: ${pattern}`);
          }
        }
      }
      return originalJson(body);
    };

    next();
  };
};

// Usage
app.post('/api/products',
  invalidateCache('cache:/api/products*', 'cache:/api/categories*'),
  productController.createProduct
);

Example: Database Query Caching

const redisClient = require('./redis');

const getCachedProducts = async (filters) => {
  const cacheKey = `products:${JSON.stringify(filters)}`;

  // Try cache
  const cached = await redisClient.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // Query database
  const products = await Product.find(filters)
    .sort('-createdAt')
    .limit(50);

  // Store in cache for 5 minutes
  await redisClient.setEx(cacheKey, 300, JSON.stringify(products));

  return products;
};

app.get('/api/products', async (req, res, next) => {
  try {
    const products = await getCachedProducts(req.query);
    res.json({ status: 'success', data: products });
  } catch (error) {
    next(error);
  }
});

Common Mistakes

  1. Caching too aggressively — Caching everything with long TTLs serves stale data. Cache selectively with appropriate TTLs based on data update frequency.
  2. Not invalidating cache on mutations — After create/update/delete, old cached responses remain. Always invalidate related cache entries after mutations.
  3. Caching user-specific data globally — Responses containing user-specific data (profile, orders) must be cached per-user, not globally.
  4. Using Redis as a primary database — Redis is an in-memory cache, not a durable database. Data is lost on restart. Store persistent data in a real database.
  5. Not handling Redis connection failures — If Redis is down, requests should fall through to the database instead of failing. Implement graceful degradation.

Practice Questions

  1. What is the purpose of TTL in caching?
  2. How do you invalidate cache entries when data changes?
  3. Why should you cache only GET requests?
  4. How do you handle Redis connection failures gracefully?
  5. Challenge: Implement a two-tier caching strategy with in-memory cache (node-cache) as L1 and Redis as L2. Cache popular products in memory with 60s TTL and all products in Redis with 300s TTL. Implement cache invalidation on product updates.

FAQ

Should I use Redis or in-memory caching?

Use in-memory caching (node-cache) for single-process apps. Use Redis for multi-process or multi-server deployments where cache must be shared.

How do I choose the TTL for cached data?

Set TTL based on how often data changes. Static reference data: hours. User profiles: minutes. Real-time data: seconds. Page views: cache rarely or not at all.

What is cache stampede and how to prevent it?

Cache stampede happens when many requests miss the cache simultaneously and all query the database. Use mutex locks or stale-while-revalidate to prevent it.

How do I cache paginated responses?

Include the page and limit in the cache key: cache:/api/products?page=1&limit=10. Invalidate all pages on data mutation.

Should I cache error responses?

No, caching error responses could serve stale errors to users. Only cache successful responses.

Mini Project

Build a Redis caching layer for a product catalog API. Implement: cache middleware for GET endpoints, TTL configuration per endpoint, cache invalidation on mutations, per-user cache for user-specific endpoints, graceful degradation when Redis is unavailable, and cache statistics endpoint (hit rate, memory usage).

What's Next

Now learn about testing with Jest and Supertest in Building REST APIs with Node.js.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro