Server-Side Caching with Redis

A response cache skips the handler, the one thing an ETag cannot do. The middleware below replaces res.json: on a miss the handler runs and its serialized body is stored under the request URL; on a hit the answer comes from the map and next is never reached.

A read-through response cache as middlewareJavaScript
function cache(ttlMs) {
  const store = new Map();
  return (req, res, next) => {
    const hit = store.get(req.originalUrl);
    if (hit && hit.expires > Date.now()) {
      return res.set('X-Cache', 'HIT').type('json').send(hit.body);
    }
    res.json = (payload) => {                    // replaced for this response only
      const body = JSON.stringify(payload);
      store.set(req.originalUrl, { body, expires: Date.now() + ttlMs });
      return res.set('X-Cache', 'MISS').type('json').send(body);
    };
    next();
  };
}
app.get('/top-cached', cache(30_000), (req, res) => res.json({ data: topBooks() }));
Output
$ node cachebench.mjs        # autocannon -c 50 -d 5, i9-7980XE, Node 25.8.0
uncached   1,228 req/s   40.14 ms avg   p99 89 ms
cached    10,903 req/s    4.10 ms avg   p99  9 ms

Nine times the throughput and a tenth of the latency, for a route whose data is thirty seconds stale. That is why caching beats micro-optimization: the fastest handler is the one that does not run.

An in-process Map is per worker, so eight cluster workers warm eight copies, and it dies with the process. Redis 2,763 fixes both, and the same client backs your sessions (Session Stores for Production) and rate limiter (Rate Limiting). Only the storage lines change, plus async/await:

The same cache in Redis with ioredis (not executed on this machine)JavaScript
const redis = new Redis(process.env.REDIS_URL);              // npm i ioredis
const key = `res:${req.user?.id ?? 'anon'}:${req.originalUrl}`;
const hit = await redis.get(key);                            // HIT branch as above
redis.setex(key, ttlSec, body).catch((e) => req.log?.warn(e));   // never block on it

A write drops what it invalidated with await redis.del(key). No Redis server is installed on the machine used for this book, so that listing is shown as code and was not executed here; every measured number above is from the in-process version. Caching and Redis covers the client and the Redis data structures.

Two rules prevent the classic bugs. Put every varying input in the key — user id, query string, Accept-Language — or you will serve one reader's data to another; and treat a cache error as a miss, so a catch that logs and continues keeps the API alive while Redis restarts.