Skip to content

Node.js Caching with Redis — Complete Guide to Performance Optimization

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Node.js Caching with Redis. We cover key concepts, practical examples, and best practices to help you master this topic.

Redis is an in-memory data structure store used for caching, session management, Rate Limiting, and real-time data in Node.js applications for high-performance data access.

What You'll Learn

By the end of this tutorial, you'll connect to Redis from Node.js, implement caching strategies with TTL, handle cache invalidation, cache database queries, store sessions, and implement rate limiting.

Why Redis Caching Matters

Database queries are slow (milliseconds). Redis reads data from memory (microseconds). Caching frequently accessed data reduces database load, lowers latency, and improves user experience.

Real-World Use

An e-commerce API caches product listings in Redis. The first request fetches from PostgreSQL (50ms) and caches it. Subsequent requests read from Redis (1ms). Cache invalidates when a product is updated.

Redis Caching Learning Path

flowchart LR
  A[File Upload] --> B[Caching Redis]
  B --> C[Docker]
  C --> D[Deployment]
  D --> E[DevOps]
  A --> F{You Are Here}
  style F fill:#f90,color:#fff

Redis Connection

npm install redis
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" });
redis.on("error", (err) => console.error("Redis error:", err));
await redis.connect();
console.log("Redis connected");

Basic Caching

async function getCachedOrFetch(key, fetchFn, ttlSeconds = 3600) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
  const data = await fetchFn();
  await redis.setEx(key, ttlSeconds, JSON.stringify(data));
  return data;
}
// Usage
app.get("/api/products", async (req, res) => {
  const products = await getCachedOrFetch("products:all", () => db.products.findAll(), 300);
  res.json(products);
});

Cache Invalidation

app.post("/api/products", async (req, res) => {
  const product = await db.products.create(req.body);
  await redis.del("products:all");  // Invalidate cache
  res.status(201).json(product);
});
app.put("/api/products/:id", async (req, res) => {
  const product = await db.products.update(req.params.id, req.body);
  await redis.del("products:all");
  await redis.del(`product:${req.params.id}`);
  res.json(product);
});

Session Storage

import { RedisStore } from "connect-redis";
app.use(session({
  store: new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true, maxAge: 86400000 }
}));

Rate Limiting with Redis

async function rateLimit(ip, maxRequests, windowSeconds) {
  const key = `rate:${ip}`;
  const current = await redis.incr(key);
  if (current === 1) await redis.expire(key, windowSeconds);
  return current <= maxRequests;
}
app.use(async (req, res, next) => {
  const allowed = await rateLimit(req.ip, 100, 60);
  if (!allowed) return res.status(429).json({ error: "Too many requests" });
  next();
});

Caching Patterns

// Cache-aside pattern
async function getUser(id) {
  const key = `user:${id}`;
  let user = await redis.get(key);
  if (user) return JSON.parse(user);
  user = await db.users.findById(id);
  if (user) await redis.setEx(key, 3600, JSON.stringify(user));
  return user;
}
// Write-through pattern
async function updateUser(id, data) {
  const user = await db.users.update(id, data);
  await redis.setEx(`user:${id}`, 3600, JSON.stringify(user));
  return user;
}

Common Mistakes

1. Caching Everything

Cache only frequently accessed, rarely changing data. Caching rarely accessed data wastes memory without performance benefit.

2. No Cache Invalidation Strategy

Stale data is worse than slow data. Always invalidate or update cache when underlying data changes.

3. Caching User-Specific Data Without Keys

If two users share the same cache key, they see each other's data. Include user ID in cache keys for personalized data.

4. Not Setting TTL

Without TTL (time-to-live), stale data lives forever. Always set reasonable expiration times.

5. Ignoring Cache Miss Storms

When cached data expires and many requests hit the database simultaneously, it creates a thundering herd. Use mutex locks for regeneration.

Practice Questions

1. Why is Redis faster than a database?

Redis stores everything in RAM (microsecond access). Databases store data on disk (millisecond access). Redis also has simpler data structures.

2. What is TTL in caching?

Time-to-Live: how long a cached entry stays valid. After TTL expires, the entry is automatically deleted, forcing a fresh fetch.

3. How do you invalidate a Redis cache?

Use redis.del(key) to remove specific keys, redis.flushDb() for full invalidation, or pattern-based deletion with SCAN + DEL.

4. What is the cache-aside pattern?

Application checks cache first. On miss, fetches from database, stores in cache, returns data. On update, invalidates cache.

5. Challenge: Implement a caching layer for a product API with automatic cache invalidation on updates.

app.get("/api/products", async (req, res) => {
  const cached = await redis.get("products");
  if (cached) return res.json(JSON.parse(cached));
  const products = await db.products.findAll();
  await redis.setEx("products", 300, JSON.stringify(products));
  res.json(products);
});
app.post("/api/products", async (req, res) => {
  const product = await db.products.create(req.body);
  await redis.del("products");
  res.status(201).json(product);
});

FAQ

Is Redis only for caching?

No. Redis is used for session stores, rate limiting, message queues (pub/sub), real-time leaderboards, and distributed locks.

Can Redis persist data to disk?

Yes. Redis supports RDB snapshots and AOF logs for persistence. But it's primarily an in-memory store.

What is the difference between Redis and Memcached?

Redis supports rich data types (strings, hashes, lists, sets), persistence, pub/sub. Memcached is simpler (strings only), multi-threaded.

How do I handle Redis connection failures?

Wrap cache reads in try/catch. On Redis failure, fall through to the database. Use Redis Sentinel or Cluster for high availability.

What is the thundering herd problem?

When many requests simultaneously miss cache and hit the database. Use mutex locking so only one request regenerates the cache.

Mini Project: Caching Proxy

Build a caching proxy server that caches external API responses in Redis.

import express from "express";
import { createClient } from "redis";
const app = express();
const redis = createClient();
await redis.connect();
app.get("/proxy/*", async (req, res) => {
  const apiUrl = req.params[0];
  const cacheKey = `proxy:${apiUrl}`;
  const cached = await redis.get(cacheKey);
  if (cached) return res.json(JSON.parse(cached));
  const response = await fetch(apiUrl);
  const data = await response.json();
  await redis.setEx(cacheKey, 300, JSON.stringify(data));
  res.json(data);
});
app.listen(3000);

What's Next

Node.js Docker Node.js Deployment Express Security

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro