On this page
  1. What you’ll build
  2. Step 1 — Set up Redis
  3. Step 2 — Cache the slow endpoint
  4. Step 3 — Invalidate on write
  5. Step 4 — Add a stampede guard
  6. Step 5 — Monitor hit rate
  7. Where AI coding assistants get this wrong
  8. Checklist
  9. FAQ
    1. What is a TTL?
    2. Why does cache invalidation matter?
    3. Should I cache everything?
  10. Related topics
  11. Sources
tutorial

How to Add Redis Caching to Your App

Add Redis caching to a Node.js app: cache a slow endpoint, invalidate on writes, and set expiry. A practical, working tutorial.

Quick answer

  • Cache a slow read endpoint in Redis: check the cache, return on hit, compute and store on miss.
  • Set a TTL so values expire, and invalidate the key whenever the data is written.
  • Success looks like a much faster second request and correct data after an update.

What you’ll build

Redis caching for a slow “get product” endpoint in a Node.js app. You’ll see the first request compute and cache, the second return instantly, and a write correctly invalidate the stale cache.

Step 1 — Set up Redis

Run Redis locally or use a managed instance, and connect from Node with the redis client:

const { createClient } = require("redis");
const redis = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" });
redis.on("error", (err) => console.error("redis", err));
await redis.connect();

Step 2 — Cache the slow endpoint

app.get("/products/:id", async (req, res) => {
  const key = `product:${req.params.id}`;

  const cached = await redis.get(key);
  if (cached) return res.json(JSON.parse(cached));

  const product = await db.getProduct(req.params.id);   // the slow query
  await redis.set(key, JSON.stringify(product), { EX: 300 }); // 5-minute TTL
  res.json(product);
});

How to verify it worked: the first request is slow, the second is fast, and redis.get returns the cached value.

Step 3 — Invalidate on write

Add invalidation wherever the product is updated:

app.put("/products/:id", async (req, res) => {
  const product = await db.updateProduct(req.params.id, req.body);
  await redis.del(`product:${req.params.id}`);   // drop the stale cache
  res.json(product);
});

How to verify it worked: update a product, then read it — the read returns the new value, not the stale cached one.

Step 4 — Add a stampede guard

For hot keys, wrap the recompute so many misses don’t all hit the database at once — for example, lock the recompute with SET key lock NX and serve the winner’s result to the others. How to verify it worked: when the key expires under load, the database sees one recompute, not a flood.

Step 5 — Monitor hit rate

Watch Redis INFO stats for keyspace_hits and keyspace_misses. A healthy cache has a high hit rate; a low one means you’re caching the wrong keys.

Where this bites vibecoders

The AI-generated version of this feature usually stops at Step 2 — cache the read, forget the invalidation — and the app then serves stale data after every write. The invalidation (redis.del) is not optional polish; it’s the difference between a cache and a bug. Always pair “cache this read” with “invalidate on that write.”

Where AI coding assistants get this wrong

  • Caching reads without invalidating on writes.
  • Using shared or un-namespaced keys that collide across users.
  • Omitting a TTL, so stale entries live forever.
  • Ignoring the stampede problem on popular keys.

Checklist

  • Cache only slow, frequently read data.
  • Namespace keys (product:123) and set a TTL.
  • Invalidate the key on every write that affects it.
  • Add stampede protection for hot keys.
  • Monitor hit rate and adjust what you cache.

FAQ

What is a TTL?

TTL (time-to-live) is the expiry you set on a cached value, after which Redis drops it. It bounds how stale data can get and prevents unbounded memory growth. Choose it based on how fresh the data must be.

Why does cache invalidation matter?

Without it, reads return old data after writes — a correctness bug that’s often subtle and intermittent. Invalidation is what keeps the cache a performance layer rather than a second, inconsistent source of truth. See What Is Caching?.

Should I cache everything?

No. Cache data that’s read often and expensive to compute, and don’t cache data that changes constantly or is cheap to fetch. A cache that mostly misses is overhead, not an optimization.

Sources

Share: