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