Node.js Redis Caching

A cache stores answers so your app can reuse them. Redis is a very fast in-memory database that serves as a popular cache. Caching makes slow pages load quickly and reduces the work on your main database.

The Notebook Idea

Suppose a friend asks you the distance to the airport. You check a map and write the answer on a sticky note. The next time someone asks, you read the note and skip the map. A cache works like that sticky note.

Without cache:  Request --> App --> Database (slow) --> Response

With cache:     Request --> App --> Redis
                                     |
                      found? --> Response (fast)
                      missing? --> Database --> save in Redis --> Response

Why Redis Is Fast

Redis keeps data in memory instead of on a disk. Memory reads take microseconds. A typical database query takes milliseconds or more.

Key Terms

  • Cache hit means Redis holds the answer.
  • Cache miss means Redis lacks the answer.
  • TTL means time to live. It sets how long Redis keeps a value.
  • Invalidation means removing old data after a change.

Set Up Redis

Install Redis on your computer or run it in Docker.

docker run -d -p 6379:6379 redis

Install the Node.js client library.

npm install redis

Connect to Redis

const { createClient } = require("redis");

const client = createClient({ url: "redis://localhost:6379" });

client.on("error", (err) => console.log("Redis error", err));

async function start() {
  await client.connect();
  console.log("Redis connected");
}
start();

Basic Commands

await client.set("greeting", "Hello");
const value = await client.get("greeting");   // Hello

await client.set("otp:asha", "482910", { EX: 60 });  // expires in 60 seconds

await client.del("greeting");
const exists = await client.exists("greeting");       // 0

The option EX: 60 sets a 60-second lifetime. Redis deletes the key by itself afterward.

Store Objects

Redis stores text. Convert objects to JSON text before saving.

const user = { id: 7, name: "Ravi" };
await client.set("user:7", JSON.stringify(user), { EX: 300 });

const saved = JSON.parse(await client.get("user:7"));

Cache-Aside Pattern

This pattern checks Redis first and falls back to the database.

async function getProduct(id) {
  const key = "product:" + id;

  const cached = await client.get(key);
  if (cached) {
    return JSON.parse(cached);              // cache hit
  }

  const product = await Product.findById(id);   // cache miss
  await client.set(key, JSON.stringify(product), { EX: 600 });
  return product;
}

Cache Middleware for Express

function cacheRoute(seconds) {
  return async (req, res, next) => {
    const key = "route:" + req.originalUrl;
    const hit = await client.get(key);

    if (hit) {
      return res.json(JSON.parse(hit));
    }

    const originalJson = res.json.bind(res);
    res.json = (body) => {
      client.set(key, JSON.stringify(body), { EX: seconds });
      return originalJson(body);
    };
    next();
  };
}

app.get("/api/products", cacheRoute(60), async (req, res) => {
  const items = await Product.find();
  res.json(items);
});

The first visitor triggers the database query. Visitors in the next 60 seconds receive the saved copy.

Remove Old Data

Cached data becomes wrong after an update. Delete the key when the source changes.

app.put("/api/products/:id", async (req, res) => {
  await Product.findByIdAndUpdate(req.params.id, req.body);
  await client.del("product:" + req.params.id);
  await client.del("route:/api/products");
  res.send("Updated");
});

Other Uses for Redis

  • Store login sessions.
  • Count requests for rate limiting.
  • Hold queues of background jobs.
  • Share messages between servers with publish and subscribe.

Good Habits

  • Set a TTL on every cached value.
  • Use clear key names such as user:7 and product:12.
  • Cache data that many people read and few people change.
  • Skip caching for private, per-user data unless the key includes the user id.
  • Let the app continue to work when Redis goes down.

Practice Tasks

  • Cache a slow function result for 30 seconds and measure the speed gain.
  • Print "hit" or "miss" in the log for each request.
  • Delete the cached value after you update the source data.