🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

How to Use Redis for Caching in a Node.js App: Complete 2026 Guide

⏱️4 min read  ·  875 words

Redis is an in-memory data store that dramatically speeds up applications by caching expensive database queries and API responses. A well-placed cache can turn a 200ms database query into a 2ms cache hit. This guide adds Redis caching to a Node.js app the right way.

Why Cache with Redis?

  • Speed: In-memory reads are 10-100x faster than database queries
  • Reduce database load: Serve repeated requests from cache, sparing your database
  • Scale: Handle traffic spikes without overwhelming your primary datastore
  • Versatile: Also handles sessions, rate limiting, queues, and pub/sub

Setup

npm install ioredis

# Run Redis locally with Docker
docker run -d --name redis -p 6379:6379 redis:7-alpine
// redis.js — connection singleton
import Redis from 'ioredis';

export const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  maxRetriesPerRequest: 3,
  retryStrategy: (times) => Math.min(times * 50, 2000),
});

redis.on('error', (err) => console.error('Redis error:', err));
redis.on('connect', () => console.log('Redis connected'));

The Cache-Aside Pattern (Most Common)

Check cache first; on a miss, fetch from the database and populate the cache:

import { redis } from './redis';

async function getUser(userId) {
  const cacheKey = `user:${userId}`;

  // 1. Try cache first
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);  // cache hit — fast path
  }

  // 2. Cache miss — fetch from database
  const user = await db.users.findById(userId);
  if (!user) return null;

  // 3. Populate cache with a TTL (expire after 1 hour)
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);

  return user;
}

Reusable Cache Wrapper

async function cached(key, ttlSeconds, fetchFn) {
  const hit = await redis.get(key);
  if (hit) return JSON.parse(hit);

  const data = await fetchFn();
  if (data !== null && data !== undefined) {
    await redis.set(key, JSON.stringify(data), 'EX', ttlSeconds);
  }
  return data;
}

// Usage — clean and reusable
const products = await cached('products:featured', 600, () =>
  db.products.findFeatured()
);

const user = await cached(`user:${id}`, 3600, () =>
  db.users.findById(id)
);

Cache Invalidation (The Hard Part)

// When data changes, invalidate the cache to avoid stale reads

async function updateUser(userId, updates) {
  const user = await db.users.update(userId, updates);

  // Invalidate the cached version
  await redis.del(`user:${userId}`);

  // Invalidate related caches too
  await redis.del('users:list');

  return user;
}

// Invalidate by pattern (use SCAN, not KEYS, in production)
async function invalidatePattern(pattern) {
  const stream = redis.scanStream({ match: pattern, count: 100 });
  stream.on('data', (keys) => {
    if (keys.length) redis.del(...keys);
  });
}
// invalidatePattern('user:*');  // clear all user caches

Express Middleware for Route Caching

function cacheRoute(ttlSeconds) {
  return async (req, res, next) => {
    const key = `route:${req.originalUrl}`;
    const cached = await redis.get(key);

    if (cached) {
      res.setHeader('X-Cache', 'HIT');
      return res.json(JSON.parse(cached));
    }

    // Override res.json to cache the response
    const originalJson = res.json.bind(res);
    res.json = (body) => {
      redis.set(key, JSON.stringify(body), 'EX', ttlSeconds);
      res.setHeader('X-Cache', 'MISS');
      return originalJson(body);
    };
    next();
  };
}

// Cache this route for 5 minutes
app.get('/api/products', cacheRoute(300), getProducts);

Rate Limiting with Redis

async function rateLimit(req, res, next) {
  const key = `ratelimit:${req.ip}`;
  const limit = 100;      // requests
  const window = 60;      // seconds

  const current = await redis.incr(key);
  if (current === 1) {
    await redis.expire(key, window);  // set TTL on first request
  }

  if (current > limit) {
    return res.status(429).json({ error: 'Too many requests' });
  }

  res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - current));
  next();
}

Caching Best Practices

  • Always set a TTL: Prevents stale data from living forever and controls memory
  • Cache what’s expensive and frequently read: Don’t cache everything — cache the hot paths
  • Handle cache failures gracefully: If Redis is down, fall through to the database, don’t crash
  • Use consistent key naming: entity:id:field conventions make invalidation predictable
  • Avoid the KEYS command in production: It blocks Redis — use SCAN instead
  • Watch for cache stampedes: When a popular key expires, many requests hit the database at once — use locks or staggered TTLs

Frequently Asked Questions

Q: What should I NOT cache?
A: Highly volatile data that changes every request, sensitive data without proper security, and data that’s cheap to compute. Cache the expensive, frequently-read, relatively-stable data.

Q: How do I choose a TTL?
A: Balance freshness vs performance. User profiles: 1 hour. Product listings: 5-10 minutes. Real-time data: seconds or don’t cache. Match TTL to how often the data actually changes.

Q: What happens if Redis goes down?
A: Your app should degrade gracefully — catch Redis errors and fall through to the database. Never let a cache failure take down your app. Wrap cache calls in try/catch.

Q: Redis vs in-memory caching (like a JS Map)?
A: In-memory Map caches are per-process (lost on restart, not shared across instances). Redis is shared across all your app instances and persists — essential for horizontally scaled apps.

Q: How do I prevent cache stampedes?
A: Use a lock (SET NX) so only one request refreshes the cache while others wait or serve stale data, or add random jitter to TTLs so popular keys don’t all expire simultaneously.

Conclusion

Redis caching can transform your Node.js app’s performance — turning slow database queries into instant cache hits. The cache-aside pattern with a reusable wrapper covers most needs, route-level middleware caches entire responses, and the same Redis instance handles rate limiting and sessions. Remember the two hard parts: always set a TTL, and invalidate the cache when data changes. Handle Redis failures gracefully, and your app gets faster and more resilient at the same time.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা