How Caching Works in Node.js
Caching is a bet: that recomputing or refetching some piece of data is expensive enough, and that data changes slowly enough, that it's worth storing a copy somewhere faster and risking that copy going stale.
Search across all documentation pages
Caching is a bet: that recomputing or refetching some piece of data is expensive enough, and that data changes slowly enough, that it's worth storing a copy somewhere faster and risking that copy going stale.
Every caching decision in a Node service - what to cache, for how long, in-process or in Redis, cache-aside or write-through - is really a decision about how much staleness risk is acceptable in exchange for how much speed and cost saved.
Caching Basics covers the concrete patterns (cache-aside, TTL strategy, stampede prevention); this page is about the trade-off underneath all of them, and specifically what Redis adds that an in-process cache can't.
Every cache sits between a reader and an origin - the actual source of truth, whether that's a database, an external API, or an expensive in-process computation - and its whole purpose is to answer reads without bothering that origin every time.
The mechanism nearly every Node service uses is cache-aside: application code checks the cache first, and only on a miss does it go to the origin, storing the result back in the cache before returning it - the cache never talks to the origin on its own, the application always mediates.
const cached = await redis.get(key);
if (cached) return JSON.parse(cached); // cache hit - origin never touched
const value = await loadFromOrigin(); // cache miss - go to the source of truth
await redis.set(key, JSON.stringify(value), "EX", 300); // store for next time
return value;The moment a value is copied into a cache, it becomes a second record of that data existing independently of the first - which means it can now be wrong in a way the origin, being the source of truth, structurally cannot.
A simple analogy: a cache is like a sticky note with a phone number copied from your address book - fast to glance at, but if the real number changes and nobody updates the sticky note, you'll confidently dial a number that no longer works.
TTL (time to live) is the blunt instrument every cache uses to bound that risk without needing to know exactly when a value changed: instead of trying to detect every write to the origin, a cached value simply expires after a fixed window, guaranteeing staleness never exceeds that window even if nothing else ever tells the cache to update.
Invalidation - actively removing or updating a cached value the moment its origin changes, rather than waiting for a TTL to expire - is the hard part of caching, and it's hard for a structural reason: the code that writes to the origin and the code that reads from the cache are often in different places, sometimes different services entirely, so nothing automatically connects "the data changed" to "the cached copy is now wrong."
The famous line about this ("there are only two hard things in computer science: cache invalidation and naming things") is a joke about a real problem: a cache with no invalidation is only ever as fresh as its TTL allows, and a cache with imperfect invalidation can serve stale data indefinitely if some write path forgets to clear the right key.
Redis's specific role in this picture is that it's a shared, out-of-process cache rather than an in-process one - and that distinction changes what kind of correctness problem you're dealing with entirely.
Without Redis (in-process cache): With Redis (shared cache):
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│instance A│ │instance B│ │instance A│ │instance B│
│ cache: {}│ │ cache: {}│ └────┬────┘ └────┬────┘
└─────────┘ └─────────┘ └──────┬──────┘
different values possible │
for the same key across instances ┌────────┐
│ Redis │ one shared value
└────────┘ per key
An in-process (in-memory) cache is fast - no network hop - but every server instance holds its own independent copy, so two instances can disagree about the same key's value at the same moment, and a write on one instance can't invalidate the copy sitting in another instance's memory.
Redis moves the cache out of the process entirely: every instance reads and writes the same shared store over the network, so there's exactly one cached value per key across the whole fleet - at the cost of a network round trip per cache access, and a new dependency that itself needs to stay available.
That network cost is also why serialization matters more with Redis than with an in-process cache: values have to be turned into a string or byte payload (usually JSON) to cross the network and back, which is real, measurable CPU and bandwidth cost that scales with how large and how frequently accessed a cached object is.
Cache stampede is what happens when a popular key expires and many concurrent requests all miss at once, all rushing to the same expensive origin call simultaneously - the exact traffic spike the cache existed to prevent, momentarily reproduced in full the instant the cache can't help.
The standard defenses are a single-flight lock (one request repopulates the cache while others wait or serve the previous value a little longer) or jittered TTLs (randomizing expiration slightly per key so many keys set at the same moment don't all expire in the same instant) - Distributed Locks covers the coordination mechanics a single-flight lock actually needs.
Cache coherence - the guarantee that all readers see the same value at the same time - is fundamentally weaker with a cache than with the origin itself, and that's not a bug to be fixed so much as a property to be reasoned about explicitly: a cache with a 5-minute TTL is promising at most 5 minutes of staleness, not promising freshness, and every consumer of that cached data needs to be able to tolerate that window.
This is also where caching connects directly to database load: a well-placed cache absorbs read traffic that would otherwise hit a connection pool with a hard concurrency ceiling, which is part of why caching and connection pool sizing are usually tuned together rather than independently - a cache that shields the database changes what "enough pool capacity" even means.
Finally, a cache should always degrade, not cascade - if Redis becomes unavailable, the correct behavior is falling back to the origin (with a warning logged), not returning errors to users; a cache going down should make a service slower, never make it fail outright, since the origin was the real source of truth all along.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| In-process (in-memory) cache | No network hop, simplest to add | Inconsistent across instances; lost on restart | Single-instance services, very hot/small lookup tables |
| Redis (shared cache) | One consistent value across all instances; survives instance restarts | Network round trip per access; a new dependency to keep available | Multi-instance deployments, shared session/rate-limit state |
HTTP Cache-Control / CDN | No origin server involvement at all for cached responses | Only fits public, non-personalized data | Static assets, public API responses |
| Write-through cache | Cache and origin updated together - lower staleness risk | Writes pay the cost of updating both systems | Data where staleness is costly and write volume is manageable |
Correctness guarantees - a cached value can be stale relative to the origin for as long as its TTL allows, or until something explicitly invalidates it. The trade is bounded staleness risk in exchange for lower latency and reduced origin load.
Application code checks the cache first; on a miss, it fetches from the origin itself and writes the result back into the cache before returning it. The cache never talks to the origin directly - the application always mediates both directions.
An in-memory object is local to one process - every server instance has its own independent copy that can disagree with the others. Redis is a separate, shared service every instance reads and writes over the network, so there's one consistent value per key across the whole fleet.
Because the code that changes the origin and the code that reads the cache are often far apart - nothing automatically connects "this data just changed" to "clear this cache key," so keeping them in sync depends on every write path remembering to invalidate the right keys.
It's when a popular cache key expires and many concurrent requests miss at the same instant, all hitting the origin simultaneously to repopulate it - reproducing exactly the load spike the cache was meant to prevent, for a brief window right at expiration.
Yes - the correct pattern is falling back to the origin (with the failure logged) so the service gets slower, not broken. A cache going down should never be able to take an otherwise-healthy origin-backed read path offline with it.
No - TTL is a trade-off dial, not a correctness setting with one right answer. Shorter TTLs reduce staleness but increase origin load; the right value depends on how much staleness that specific data can tolerate.
Yes, when placed well - reads served from cache never reach the pool at all, which is why cache placement and connection pool sizing are often tuned together rather than treated as unrelated concerns.
Redis values travel over a network connection, so they have to be serialized (usually to JSON) and deserialized on every access - real CPU and bandwidth cost that an in-process object reference never pays, since it's already sitting in the same process's memory.
They solve the same general problem at different layers - Cache-Control/CDN caching works for public, non-personalized HTTP responses cached outside the origin server entirely, while Redis caching typically handles personalized or auth-gated data that a public CDN can't safely cache.
No - Redis solves cross-instance consistency (one shared value per key), not staleness relative to the origin. A value in Redis can still be stale within its TTL window exactly the same way an in-process cached value can.
Stack versions: This page was written for Node.js 24 LTS, npm 10+, and TypeScript 5.6+.
Reviewed by Chris St. John·Last updated Jul 15, 2026