What are Caching Strategies in Node.js?
Learn Node.js caching strategies — in-memory, Redis, and HTTP caching with TTL and invalidation — to cut latency, reduce load, and scale your backend.
Expected Interview Answer
Caching strategies in Node.js store the result of expensive operations so repeated requests are served from fast memory or a shared store instead of recomputing or re-fetching, cutting latency and load.
Common approaches are in-process caching (a Map or an LRU cache living inside the Node process), distributed caching with Redis or Memcached shared across instances, and HTTP-level caching via Cache-Control headers, ETags and a CDN. Each entry needs an eviction and invalidation policy — TTL expiry, LRU eviction, or explicit busting on write — otherwise you serve stale data. In-process caches are fastest but not shared and lost on restart; Redis is shared and durable but adds a network hop.
- Reduces response latency for repeated requests
- Lowers database and downstream API load
- Improves throughput and scalability
- Cuts cost by avoiding redundant computation
- Smooths out traffic spikes
AI Mentor Explanation
A team analyst does not re-watch every past match live each time the captain asks about an opponent; they keep a prepared dossier of pre-computed stats and pull answers instantly. Caching in Node.js works the same way: the first expensive lookup is computed once and stored, so every later request is answered from the ready-made dossier instead of replaying the whole innings again.
Step-by-Step Explanation
Step 1
Identify hot, expensive reads
Profile which routes or queries are slow and frequently repeated with the same inputs — these are cache candidates.
Step 2
Choose a cache layer
Pick in-process (Map/LRU) for single-instance speed, or Redis/Memcached when multiple Node instances must share the cache.
Step 3
Design the cache key
Build a deterministic key from the inputs (e.g. `user:42:orders`) so the same request always maps to the same entry.
Step 4
Set expiry and eviction
Apply a TTL and an eviction policy (LRU, max size) so memory is bounded and data does not go stale forever.
Step 5
Handle invalidation
On writes, delete or update affected keys so readers do not see outdated values.
Step 6
Add HTTP caching
Use Cache-Control, ETag and a CDN for static or public responses to offload traffic before it reaches Node.
What Interviewer Expects
- Difference between in-process and distributed caching
- Awareness of TTL, LRU eviction and cache invalidation
- Knowledge of Redis for multi-instance sharing
- Understanding of HTTP caching (Cache-Control, ETag, CDN)
- Recognising cache stampede and stale-data risks
Common Mistakes
- Using an in-process cache across multiple instances and expecting it to be shared
- Never setting a TTL, causing unbounded memory growth
- Forgetting to invalidate the cache on writes and serving stale data
- Caching per-user private data under a shared public key
- Ignoring cache stampede when many requests miss at once
Best Answer (HR Friendly)
“Caching in Node.js means saving the results of slow or repeated work so the app can hand them back instantly instead of redoing the effort. It makes the application faster and cheaper to run, as long as you refresh or clear the saved data when it changes.”
Code Example
const cache = new Map()
function getCached(key, ttlMs, loader) {
const hit = cache.get(key)
if (hit && hit.expires > Date.now()) {
return Promise.resolve(hit.value)
}
return Promise.resolve(loader()).then((value) => {
cache.set(key, { value, expires: Date.now() + ttlMs })
return value
})
}
// Usage: cache an expensive DB read for 60 seconds
app.get('/users/:id', async (req, res) => {
const user = await getCached(
`user:${req.params.id}`,
60_000,
() => db.users.findById(req.params.id),
)
res.json(user)
})import { createClient } from 'redis'
const redis = createClient()
await redis.connect()
async function getUser(id) {
const cached = await redis.get(`user:${id}`)
if (cached) return JSON.parse(cached)
const user = await db.users.findById(id)
// EX sets a 60s TTL so the entry auto-expires
await redis.set(`user:${id}`, JSON.stringify(user), { EX: 60 })
return user
}
// Invalidate on write so readers never see stale data
async function updateUser(id, patch) {
const user = await db.users.update(id, patch)
await redis.del(`user:${id}`)
return user
}Follow-up Questions
- What is a cache stampede and how do you prevent it?
- When would you choose Redis over an in-process cache?
- How do ETags and Cache-Control headers work together?
- What is the difference between write-through and write-back caching?
- How do you keep caches consistent across multiple Node instances?
MCQ Practice
1. Why is an in-process Map cache a poor fit for a horizontally scaled Node.js app?
An in-process cache lives in one instance's memory; other instances have separate copies, so data can diverge. Redis provides a shared store.
2. What does a TTL on a cache entry primarily control?
TTL (time to live) sets how long an entry stays valid before it expires and must be refreshed, bounding staleness and memory.
3. Which is the correct action when the underlying data is updated?
On a write you must invalidate (delete) or update the affected keys, otherwise readers continue to receive the old, stale value.
Flash Cards
In-process vs distributed cache — In-process (Map/LRU) is fastest but per-instance and lost on restart; distributed (Redis) is shared across instances and survives restarts but adds a network hop.
TTL — Time to live — how long a cache entry stays valid before it expires and is refetched, bounding how stale data can get.
Cache invalidation — Deleting or updating cached keys when the source data changes so readers do not receive outdated values.
Cache stampede — Many requests miss the same expired key at once and all hit the origin together; mitigated with locks, request coalescing, or stale-while-revalidate.
HTTP caching — Cache-Control, ETag and CDNs let responses be cached before reaching Node, offloading traffic for public or static content.