HTTP Caching and Redis Response Cache

Eliminate Redundant Database Hits with ETags, Cache-Control, and Redis

HTTP Caching and Redis Response Cache

Layer HTTP caching into a Koa 2 app using koa-conditional-get and koa-etag for 304 responses, then add a Redis-backed middleware cache to serve repeated requests without touching the database.

5 min read Level 3/5 #koa#data#caching
What you'll learn
  • Enable ETag-based conditional GET responses with koa-etag and koa-conditional-get
  • Set Cache-Control headers to control browser and CDN caching behaviour
  • Build a Redis response cache middleware that short-circuits downstream handlers

Caching is the fastest way to scale a Koa API. Two complementary strategies exist: HTTP caching (the browser and CDN avoid round-trips entirely) and server-side response caching (the server skips expensive database queries).

HTTP Caching: ETag + Conditional GET

npm install koa-etag koa-conditional-get

Add both middleware near the top of the stack, before your router:

import Koa from 'koa';
import conditional from 'koa-conditional-get';
import etag        from 'koa-etag';

const app = new Koa();

app.use(conditional());  // checks If-None-Match / If-Modified-Since
app.use(etag());         // adds ETag header based on response body hash

When a browser or CDN sends a request with If-None-Match: "<etag>", koa-conditional-get compares it against the current ETag. If they match, the response is truncated to a 304 Not Modified — no body is transmitted.

Cache-Control Headers

Set Cache-Control per route to tell clients how long to cache a response.

router.get('/products', async (ctx) => {
  ctx.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
  ctx.body = await productService.list();
});

router.get('/me', async (ctx) => {
  ctx.set('Cache-Control', 'private, no-store');  // never cache user-specific data
  ctx.body = await userService.getById(ctx.state.userId);
});
DirectiveMeaning
publicShared caches (CDN) may store the response
privateOnly the browser may cache; CDN must not
max-age=NCache is fresh for N seconds
no-storeNever cache — fetch fresh every time
stale-while-revalidate=NServe stale while fetching fresh in the background

Redis Response Cache Middleware

For expensive queries, cache the serialised response in Redis and serve it directly on repeat requests.

npm install ioredis
// middleware/redisCache.js
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export function responseCache(ttlSeconds = 60) {
  return async (ctx, next) => {
    if (ctx.method !== 'GET') return next();

    const key    = `cache:${ctx.url}`;
    const cached = await redis.get(key);

    if (cached) {
      ctx.set('X-Cache', 'HIT');
      ctx.type = 'application/json';
      ctx.body = cached;
      return;
    }

    await next();

    if (ctx.status === 200 && ctx.body) {
      const serialised = typeof ctx.body === 'string'
        ? ctx.body
        : JSON.stringify(ctx.body);
      await redis.setex(key, ttlSeconds, serialised);
      ctx.set('X-Cache', 'MISS');
    }
  };
}

Apply it to specific routes:

import { responseCache } from '../middleware/redisCache.js';

router.get('/products', responseCache(120), async (ctx) => {
  ctx.body = await productService.list();
});

Cache Invalidation

Invalidate the Redis key whenever the underlying data changes:

router.post('/products', async (ctx) => {
  const product = await productService.create(ctx.request.body);
  await redis.del('cache:/products');   // bust the list cache
  ctx.status = 201;
  ctx.body   = product;
});

Up Next

Learn how to offload slow or resource-intensive work to a BullMQ background queue so your handlers respond immediately.

Background Jobs with BullMQ →