Suggest an editImprove this articleRefine the answer for “How to implement caching in Express.js for better performance?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Express.js caching** stores results of expensive operations (DB queries, external API calls) and returns the stored copy on repeat requests, cutting response time and database load. ```js const NodeCache = require('node-cache'); const cache = new NodeCache({ stdTTL: 300 }); const hit = cache.get(req.originalUrl); if (hit) return res.json(hit); // skip handler // on response: cache.set(key, body, ttl) ``` **Key:** node-cache for single-server apps, Redis when running multiple instances, and Cache-Control headers for browser and CDN caching.Shown above the full answer for quick recall.Answer (EN)Image**Express.js caching** stores the result of expensive operations (DB queries, external API calls) and returns that stored copy on repeat requests instead of doing the work again. ## Theory ### TL;DR - Three layers to know: in-process memory (node-cache), distributed cache (Redis), and HTTP headers for browsers and CDNs - node-cache is fast but disappears when the process restarts and does not share state between server instances - Redis survives restarts and works across all Node instances behind a load balancer - Cache invalidation on writes is where most production bugs live - Start with short TTLs (30-60s) and increase based on how often the data actually changes ### Quick example ```js const NodeCache = require('node-cache'); const cache = new NodeCache({ stdTTL: 300 }); // 5-minute default TTL function cacheMiddleware(duration) { return (req, res, next) => { const key = req.originalUrl; const hit = cache.get(key); if (hit) return res.json(hit); // cache hit - skip handler entirely const originalJson = res.json.bind(res); res.json = (body) => { cache.set(key, body, duration); // store before sending originalJson(body); }; next(); }; } app.get('/api/products', cacheMiddleware(300), async (req, res) => { const products = await db.query('SELECT * FROM products'); res.json(products); // this also writes to cache }); ``` The middleware intercepts `res.json`, stores the body before sending, and on the next request for the same URL returns the stored copy directly. ### In-memory vs Redis node-cache stores data inside the same Node.js process. No network call, so it is the fastest option. But it has two hard limits: the cache is gone when the process restarts, and if you run multiple instances (load balancer, PM2 cluster mode), each instance has its own separate copy. Redis is a separate service. Every Node instance connects to the same Redis, so the cache is shared. It survives restarts, scales to any number of instances, and handles TTL natively. The trade-off is a network round-trip, typically 1-2ms locally and 5-15ms in cloud environments. Many teams run node-cache in development and Redis in production. That works fine as long as you hide the cache layer behind a consistent interface so switching is a one-line change. ### How HTTP cache headers work The third layer is the client side. Setting `Cache-Control` on responses tells browsers and CDNs not to make the request at all if they have a fresh copy. ```js // Public data - CDN and browser can both cache it app.get('/api/products', (req, res) => { res.set('Cache-Control', 'public, max-age=300'); // 5 minutes res.json(products); }); // User-specific data - only the browser, not CDNs app.get('/api/profile', authMiddleware, (req, res) => { res.set('Cache-Control', 'private, no-cache'); res.json(user); }); ``` ETags go one step further. The server sends a hash of the response body. The browser sends that hash back on the next request. If nothing changed, the server returns 304 with an empty body. ```js const etag = require('etag'); app.get('/api/config', (req, res) => { const data = getAppConfig(); const tag = etag(JSON.stringify(data)); if (req.headers['if-none-match'] === tag) { return res.status(304).end(); // no body sent } res.set('ETag', tag); res.set('Cache-Control', 'public, max-age=60'); res.json(data); }); ``` For large responses like configuration objects or reference data, this cuts bandwidth significantly on repeat visits. ### Cache invalidation This is where things get messy in production. Any write operation should clear the cached version of that resource immediately. ```js app.post('/api/products', async (req, res) => { const product = await createProduct(req.body); // Remove stale data from both caches cache.del('/api/products'); await redis.del('cache:/api/products'); res.status(201).json(product); }); ``` For pattern-based invalidation in Redis (paginated routes like `/api/products?page=1`): ```js async function invalidatePattern(pattern) { const keys = await redis.keys(pattern); // use SCAN in production if (keys.length > 0) { await redis.del(...keys); } } await invalidatePattern('cache:/api/products*'); ``` `redis.keys()` blocks the event loop on large key sets. In production with thousands of cached keys, replace it with `redis.scanStream()`. ### Caching strategy comparison | Strategy | Speed | Scales across instances | Survives restarts | Best for | |---|---|---|---|---| | node-cache | Fastest | No | No | Single server, dev | | Redis | Very fast | Yes | Yes | Production APIs | | HTTP headers | Client-side | CDN-friendly | Browser | Public, static content | | CDN | Fastest for users | Global | Edge | Assets, public APIs | ### What to cache and what not to Cache database query results, external API responses, and computed aggregations. Anything expensive to regenerate and not unique per user is a good candidate. Avoid caching authentication tokens, real-time data like prices or inventory counts, and user-specific responses unless the user ID is part of the cache key. ### Common mistakes **1. Using `req.path` instead of `req.originalUrl` as the cache key** `req.path` strips query params, so `/api/products?category=shoes` and `/api/products?category=hats` both map to the same cache entry. `req.originalUrl` includes the full query string, so use that. **2. Caching error responses** ```js // Wrong: caches 500 errors too res.json = (body) => { cache.set(key, body, duration); originalJson(body); }; // Right: only store successful responses res.json = (body) => { if (res.statusCode >= 200 && res.statusCode < 300) { cache.set(key, body, duration); } originalJson(body); }; ``` **3. No fallback when Redis is down** If Redis is unavailable and there is no try-catch around the read, the middleware throws and the whole API goes down. Always catch Redis errors and call `next()` to fall through to the handler. **4. Forgetting PUT and PATCH operations** Most devs remember to invalidate on POST. Updates via PUT or PATCH also write data, so they also need to clear the cache. **5. Using `redis.keys()` in production** `KEYS *pattern*` scans the entire Redis keyspace synchronously and blocks all other operations. Use `SCAN` with a cursor for any dataset with more than a few hundred keys. ### Real-world usage - Express APIs with public product catalogs: Redis with 5-10 minute TTL - Rate limiting alongside response caching: Redis handles both with separate key namespaces - Next.js API routes (Node backend): same middleware patterns, same Redis setup - GraphQL servers on Express: cache by query hash, not by URL - High-traffic public endpoints: HTTP headers and CDN as the first layer, Redis as the second ### Follow-up questions **Q:** How do you cache user-specific data without serving one user's data to another? **A:** Include the user ID in the cache key: `` `cache:${req.originalUrl}:${req.user.id}` ``. Each user gets a separate entry. It costs more memory but is safe. **Q:** What is the difference between `max-age` and `s-maxage` in Cache-Control? **A:** `max-age` applies to every cache including the browser. `s-maxage` applies only to shared caches like CDNs. You can combine them: `public, max-age=60, s-maxage=300` means the browser holds it for 1 minute but the CDN keeps it for 5. **Q:** When would you cache at the database layer instead of at the application layer? **A:** When queries are highly varied (many filter combinations), application-level caching creates too many keys and most of them are hit only once. Database-level query caching or read replicas handle this better. Application caching works best for a finite set of expensive, frequently repeated queries. **Q:** How do you handle cache warming after a Redis restart or a fresh deploy? **A:** Either accept the cold start where the first requests are slow while the cache fills back up, or write a warmup script that hits critical endpoints right after deploy. There is no universal answer. It depends on how large the performance penalty is during that cold window for your specific traffic pattern. ## Examples ### In-memory caching with a debug header ```js const express = require('express'); const NodeCache = require('node-cache'); const app = express(); const cache = new NodeCache({ stdTTL: 300, checkperiod: 60 }); function cacheMiddleware(duration) { return (req, res, next) => { const key = req.originalUrl; const hit = cache.get(key); if (hit) { res.set('X-Cache', 'HIT'); return res.json(hit); } res.set('X-Cache', 'MISS'); const originalJson = res.json.bind(res); res.json = (body) => { if (res.statusCode >= 200 && res.statusCode < 300) { cache.set(key, body, duration); } originalJson(body); }; next(); }; } app.get('/api/products', cacheMiddleware(300), async (req, res) => { const products = await db.query('SELECT * FROM products LIMIT 100'); res.json(products); }); ``` The `X-Cache` header tells you whether the response came from cache or the database. Useful during load testing or when debugging unexpected stale data. ### Redis caching with graceful degradation ```js const Redis = require('ioredis'); const redis = new Redis(process.env.REDIS_URL); function redisCacheMiddleware(ttl = 300) { return async (req, res, next) => { const key = `cache:${req.originalUrl}`; try { const cached = await redis.get(key); if (cached) { return res.json(JSON.parse(cached)); } } catch (err) { // Redis is unavailable - skip cache, go to handler console.error('Redis read error:', err); return next(); } const originalJson = res.json.bind(res); res.json = async (body) => { try { if (res.statusCode >= 200 && res.statusCode < 300) { await redis.setex(key, ttl, JSON.stringify(body)); } } catch (err) { console.error('Redis write error:', err); // Do not block the response } originalJson(body); }; next(); }; } // Invalidate on write app.post('/api/products', async (req, res) => { const product = await createProduct(req.body); await redis.del('cache:/api/products'); res.status(201).json(product); }); ``` If Redis goes down, the request falls through to the handler. The API keeps working, just without the caching layer until Redis recovers. ### ETag-based conditional requests ```js const etag = require('etag'); app.get('/api/config', (req, res) => { const config = getAppConfig(); // expensive config load const tag = etag(JSON.stringify(config)); // Browser sends back the ETag it got last time if (req.headers['if-none-match'] === tag) { return res.status(304).end(); // use your cached copy } res.set('ETag', tag); res.set('Cache-Control', 'public, max-age=60'); res.json(config); }); ``` A 304 response has no body. For large configuration objects or reference datasets that rarely change, this removes the download entirely on repeat visits and drops bandwidth noticeably.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.