Node.js

Caching

In-memory and Redis-backed caching strategies for Node.js — node-cache for simple use cases, ioredis for production-grade caching, Express cache middleware, TTL management, and cache invalidation.

In-Memory Cache with node-cache

node-cache is a zero-dependency in-memory store. Great for single-process apps or when Redis is overkill. Data is lost on restart.

npm install node-cache

// lib/cache.js
const NodeCache = require('node-cache');

// stdTTL: default TTL in seconds (0 = no expiry)
// checkperiod: how often expired keys are deleted (seconds)
const cache = new NodeCache({ stdTTL: 300, checkperiod: 60 });

/**
 * Get or set cache (cache-aside pattern)
 * @param {string}   key
 * @param {Function} fetchFn  - async function that fetches fresh data
 * @param {number}   ttl      - override TTL in seconds
 */
async function getOrSet(key, fetchFn, ttl = 300) {
    const cached = cache.get(key);
    if (cached !== undefined) {
        console.log(`[CACHE HIT] ${key}`);
        return cached;
    }

    console.log(`[CACHE MISS] ${key}`);
    const data = await fetchFn();
    cache.set(key, data, ttl);
    return data;
}

function invalidate(key)    { cache.del(key); }
function invalidateAll()    { cache.flushAll(); }
function stats()            { return cache.getStats(); }

module.exports = { cache, getOrSet, invalidate, invalidateAll, stats };

Using node-cache in a Route

Wrap any expensive operation (DB query, external API call) with getOrSet. Invalidate the key when the underlying data changes.

const { getOrSet, invalidate } = require('../lib/cache');
const db = require('../db');

// Cached GET
router.get('/products', async (req, res) => {
    const data = await getOrSet('products:all', () =>
        db.query('SELECT * FROM products ORDER BY name').then(([rows]) => rows),
    300); // 5-minute TTL

    res.json(data);
});

// Invalidate on mutation
router.post('/products', async (req, res) => {
    const [result] = await db.query(
        'INSERT INTO products SET ?', [req.body]
    );
    invalidate('products:all'); // bust the cache
    res.status(201).json({ id: result.insertId });
});

Redis Cache with ioredis

ioredis is the recommended Redis client. It supports Sentinel, Cluster, and Lua scripting. Always serialize values to JSON when storing objects.

npm install ioredis

// lib/redis.js
const Redis = require('ioredis');

const redis = new Redis({
    host:          process.env.REDIS_HOST || '127.0.0.1',
    port:          Number(process.env.REDIS_PORT) || 6379,
    password:      process.env.REDIS_PASSWORD || undefined,
    db:            Number(process.env.REDIS_DB) || 0,
    retryStrategy: (times) => Math.min(times * 50, 2000),
});

redis.on('connect', () => console.log('[Redis] connected'));
redis.on('error',   (err) => console.error('[Redis] error', err.message));

/**
 * Get or set with JSON serialization
 */
async function getOrSet(key, fetchFn, ttlSeconds = 300) {
    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;
}

async function invalidate(key)        { await redis.del(key); }
async function invalidatePattern(pat) {
    const keys = await redis.keys(pat); // e.g. 'products:*'
    if (keys.length) await redis.del(...keys);
}

module.exports = { redis, getOrSet, invalidate, invalidatePattern };

Express Cache Middleware

A reusable middleware factory that caches the full JSON response for a route. Skips cache for non-GET requests automatically.

// middleware/cacheMiddleware.js
const { redis } = require('../lib/redis');

/**
 * Cache middleware factory
 * Usage: router.get('/products', cacheMiddleware(120), handler)
 * @param {number} ttl - cache duration in seconds
 */
function cacheMiddleware(ttl = 300) {
    return async (req, res, next) => {
        if (req.method !== 'GET') return next();

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

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

        // Intercept res.json to cache the response
        const originalJson = res.json.bind(res);
        res.json = (body) => {
            redis.setex(key, ttl, JSON.stringify(body)).catch(console.error);
            return originalJson(body);
        };

        next();
    };
}

module.exports = cacheMiddleware;

// --- Usage ---
// const cacheMiddleware = require('../middleware/cacheMiddleware');
// router.get('/products', cacheMiddleware(120), getProducts);

TTL Strategies & Cache Key Conventions

Consistent key naming prevents collisions across services. Use prefixes and versioning to make invalidation simple.

// Cache key conventions
const KEYS = {
    productList:   ()         => 'v1:products:list',
    productById:   (id)       => `v1:products:${id}`,
    userSession:   (userId)   => `v1:sessions:${userId}`,
    categoryTree:  ()         => 'v1:categories:tree',
};

// TTL constants (seconds)
const TTL = {
    SHORT:   60,        // 1 minute  – frequently changing data
    MEDIUM:  300,       // 5 minutes – semi-static
    LONG:    3600,      // 1 hour    – rarely changing
    DAY:     86400,     // 1 day     – static reference data
};

// Example: cache a product by ID for 5 minutes
async function getCachedProduct(id) {
    return getOrSet(KEYS.productById(id), () => fetchProductFromDB(id), TTL.MEDIUM);
}

// Bump cache version to invalidate all v1 keys at once
// Simply change 'v1' to 'v2' in KEYS constants