Add Redis Caching to Your Node.js Application
Nov 13, 2026 • 6 min read

You add Redis because the API feels slow. Then someone updates a record and the old value keeps appearing. That’s the part worth designing before you cache the busiest endpoint. Decide which answers may be a little stale, how long that is acceptable, and what your app should do when Redis isn’t available.
Choose what to cache, who each key belongs to, how long it lives, and what updates should invalidate it. Also decide how your app behaves without Redis. Watch the answers, database load, and latency together; a high hit rate won’t help if customers keep seeing the wrong data.
Start with one cacheable read
- Measure the uncached operation and identify its permitted staleness window and authorization scope.
- Choose an explicit key structure and TTL, and cache only a bounded, serializable result.
- Invalidate or version entries when authoritative data changes; test concurrent writes and cache fills.
- Exercise a cache outage and simultaneous misses while observing database load and request timeouts.
Pick a caching approach that fits the read
| Approach | Choose it when | Verification or tradeoff |
|---|---|---|
| Cache-aside | Reads can tolerate the defined freshness window | Handle misses and update races. |
| Versioned keys | Readers can identify authoritative versions | Plan version discovery and old-key expiry. |
| No cache | The query is already fast or strongly fresh | Optimize the underlying operation first. |
A small example to try
// client is an already connected node-redis client.
// Authorize the caller before accessing tenant-scoped data.
export async function cachedProduct(client, tenantId, scopeId, productId, loadProduct) {
if ([tenantId, scopeId, productId].some(v => typeof v !== 'string' || !v)) {
throw new Error('Validated scope identifiers are required');
}
const key = `product:${encodeURIComponent(tenantId)}:${encodeURIComponent(scopeId)}:${encodeURIComponent(productId)}`;
const hit = await client.get(key);
if (hit !== null) return JSON.parse(hit);
const product = await loadProduct(tenantId, scopeId, productId);
if (product !== null) {
await client.set(key, JSON.stringify(product), { EX: 60 });
}
return product;
}The server must derive and validate scopeId from the caller’s authorized data scope, not accept it as an authorization claim from the browser. Cache only results that are identical for callers within that scope. The sixty-second TTL is illustrative, not a universal freshness recommendation. This minimal happy path makes key scope and the source read explicit. Add bounded client timeouts, an intentional cache-outage policy, invalidation, and controlled concurrent refills before using the pattern for an important workload. Do not infer those failure guarantees from the small example. node-redis documentation.
How do you verify the cache key's authorization scope?
The example takes a server-derived scopeId because tenant membership alone may not determine which fields a caller can see. A product record visible to one role can contain information another role should not receive. Cache only a representation shared by every authorized caller in the chosen scope, or include the additional authorization dimension in the key.
Do not accept a browser-supplied scope identifier as proof of permission. Authorize the request first, derive the supported data scope on the server, and pass that scope to both the cache lookup and the authoritative loader. Otherwise, changing a key can become a way to request a representation the caller was never entitled to read.
Which small cache tests catch expensive mistakes?
| Test | Expected property | What a failure means |
|---|---|---|
| Read the same product in two tenants | Each tenant receives its own authorized representation | The key or loader omitted tenant scope |
| Read under different permission scopes | Data follows your app's authorization contract | A shared cache representation is too broad |
| Update the authoritative record | Responses follow the chosen freshness policy | The invalidation or version strategy is incomplete |
| Issue simultaneous misses | Refill demand remains within the database budget | A cache miss can amplify into an uncontrolled load spike |
| Make the cache unavailable | The request follows the deliberate fallback or failure policy | The happy-path cache call became an unbounded dependency |
A cache failure is also a useful test of whether the optimization is truly optional. If the database cannot absorb even the planned fallback load, choose a degraded-service policy or a controlled refill mechanism before production. Do not describe an unconditional fallback as resilient without measuring the work it transfers to the database.
Keep the example TTL separate from the product's actual consistency requirement. Sixty seconds is an implementation illustration. Inventory which updates, deletes, and permission changes must invalidate or version a representation, then verify those operations with the documented Redis client.
What should your app do with a malformed cached value?
Define a policy for values that cannot be decoded into the expected representation. The minimal example deliberately leaves production error handling to your app; it should not be interpreted as a recommendation to let every parsing error become an unexplained request failure.
If the authoritative value can be loaded safely, your app may choose a controlled removal and refill path. Keep that work bounded so a bad cached representation does not generate an uncontrolled number of database requests. If falling back is unsafe for the operation, expose the defined failure instead.
Include representation changes in release planning. A new application version should not silently interpret an old cached shape as a different authorized result. Version the key or representation where the compatibility requirement warrants it, and test mixed-version behaviour during deployment.
A cache hit can still return the wrong answer
Start with one expensive, frequently repeated read rather than caching every endpoint. Category data, reference data, or computed results can have different freshness needs. A cache entry must identify the tenant and other dimensions that change the answer; performance work must not merge data belonging to different users.
Questions you might have
What happens when Redis is unavailable?
Your app needs an explicit failure policy rather than an accidental long wait. A database fallback can preserve reads if its additional load is controlled. Use bounded cache timeouts, limit refill concurrency, and decide which operations can fail or serve an accepted stale value without violating correctness.
How should I choose a TTL?
Choose a TTL from the permitted freshness window and the cost of refilling the value. A short lifetime can increase database load; a long one can expose stale information. Test update paths and concurrent misses, because setting a TTL does not by itself guarantee immediately fresh results.
Should every database read be cached?
Cache reads whose repetition and cost justify the additional consistency work. First inspect query efficiency and measure a realistic baseline. Avoid caching sensitive or user-specific results without correct key scoping, and retain a clear source of truth so an unavailable or stale cache does not corrupt application behaviour.
Keep reading
Separate Background Workers from Your Web Application; PostgreSQL Connection Pooling: Prevent Connection Exhaustion; PostgreSQL Slow Query Troubleshooting Before a Server Upgrade; Redis Cache Invalidation: Practical Patterns for Application Developers.
Build confidence one read at a time
Start with one read path whose freshness requirements you understand. Measure its latency, test an update, and try the same request with the cache unavailable. Once those outcomes make sense, you’ll have a pattern worth extending. Adding Redis to every endpoint at once makes it harder to see which assumption caused the next bug.
Sources and further reading
The official node-redis guide documents a Node.js client for Redis; cache consistency remains an application responsibility rather than a client-library guarantee. Redis documentation.
Best practise for global caching in Redis and Nestjs ? describes a related situation: A NestJS developer asks how to refresh globally cached category data in a PostgreSQL-backed application.