15 questions found
What is caching, and why is it commonly used to improve the performance of a Node.js application?
Beginner
Caching stores the result of an expensive operation (a database query, an API call, a computed value) temporarily, so subsequent requests for the same data can be served from the fast cache instead of repeating the expensive work -- reducing latency for end users and reducing load on downstream systems like databases, which is especially valuable for data that's read far more often than it changes.
const cache = new Map();
async function getUser(id) {
if (cache.has(id)) return cache.get(id);
const user = await db.users.findById(id);
cache.set(id, user);
return user;
}
Real-world example
A product-catalog API that previously queried the database on every request starts serving the vast majority of requests from an in-memory cache instead, since product data changes only a few times a day but is read thousands of times per minute.
Common follow-ups: What's the risk of caching data that changes frequently, and how do you decide what's safe to cache?;What is cache invalidation, and why is it often called one of the two hardest problems in computer science?
Databases & ORMs (MongoDB/Mongoose
SQL/Sequelize);Performance Optimization & Profiling
What is the difference between in-memory caching (like a Map or node-cache) and a distributed cache (like Redis)?
Intermediate
An in-memory cache lives entirely within a single Node.js process's memory -- extremely fast with no network overhead, but limited to that one process (useless once an application scales to multiple instances) and lost entirely on restart. A distributed cache like Redis runs as a separate shared service that all application instances connect to, providing a single consistent cache shared across every instance, at the cost of network latency per access and an additional piece of infrastructure.
// In-memory: fast, but each of N server instances has its own separate cache
const cache = new Map();
// Distributed: one shared cache across every instance
const redis = require('redis').createClient();
await redis.set(`user:${id}`, JSON.stringify(user), { EX: 3600 });
const cached = await redis.get(`user:${id}`);
Real-world example
A service running behind a load balancer with five instances switches from an in-memory Map cache to Redis, since with the in-memory approach a cache-invalidation event on one instance had no way to also clear the stale entry sitting in the other four instances' separate caches.
Common follow-ups: What's a hybrid caching strategy that uses both layers together, and why might that be worth the added complexity?;How does Redis's own memory limit and eviction policy affect what you can safely rely on it for?
Caching with Redis;Cloud & DevOps
What are the main cache invalidation strategies (TTL, write-through, cache-aside), and when is each appropriate?
Advanced
Time-to-live (TTL) expiration simply lets cached entries expire automatically after a fixed duration -- simple, but can serve stale data until expiration. Cache-aside (lazy loading) has the application check the cache first, falling back to the source and populating the cache on a miss -- flexible, but the first request after a miss pays the full latency cost. Write-through updates the cache synchronously every time the underlying data is written, keeping the cache always consistent at the cost of added write latency and complexity.
// Cache-aside pattern
async function getProduct(id) {
let product = await redis.get(`product:${id}`);
if (!product) {
product = await db.products.findById(id);
await redis.set(`product:${id}`, JSON.stringify(product), { EX: 600 });
}
return typeof product === 'string' ? JSON.parse(product) : product;
}
// Write-through: update cache and DB together on every write
async function updateProduct(id, data) {
await db.products.update(id, data);
await redis.set(`product:${id}`, JSON.stringify(data), { EX: 600 });
}
Real-world example
A pricing service uses write-through caching specifically for currency exchange rates (needing immediate consistency everywhere), but uses simple cache-aside with a short TTL for less critical, less frequently accessed product descriptions.
Common follow-ups: What is the 'thundering herd' problem with TTL-based caching, and how do you mitigate it?;How would you combine cache-aside with an explicit invalidation event to get both simplicity and freshness?
Databases & ORMs (MongoDB/Mongoose
SQL/Sequelize);Message Queues (RabbitMQ & Kafka)
What is cache stampede (thundering herd), and how do you prevent it in a high-traffic Node.js service?
Advanced
Cache stampede occurs when a popular cached entry expires and many concurrent requests simultaneously discover the cache miss and all rush to recompute the same expensive underlying data at once, momentarily overwhelming the downstream database exactly as if the cache didn't exist. Common mitigations include using a lock or 'in-flight request' map so only the first request actually recomputes the value while others wait for that same result, and 'stale-while-revalidate' -- serving the slightly stale cached value immediately while refreshing it in the background.
const inFlight = new Map();
async function getWithStampedeProtection(key, fetchFn) {
if (inFlight.has(key)) return inFlight.get(key); // join the existing in-flight fetch
const promise = fetchFn().finally(() => inFlight.delete(key));
inFlight.set(key, promise);
return promise;
}
Real-world example
A news site's homepage data cache expiring during a traffic spike used to cause a brief but severe database overload; adding an in-flight deduplication map ensures only one regeneration happens per expiration, with all other concurrent requests simply awaiting that same result.
Common follow-ups: How does 'stale-while-revalidate' differ from the in-flight deduplication approach, and could they be combined?;What's the risk of the in-flight deduplication approach if the single in-progress fetch itself hangs or fails?
Performance Optimization & Profiling;Error Handling
What HTTP caching headers (Cache-Control, ETag) can a Node.js/Express API use to let clients and CDNs cache responses?
Intermediate
Cache-Control specifies directives like max-age (how long a response can be cached) and public/private (whether shared caches like CDNs may store it); ETag provides a unique fingerprint of a response version, letting a client send If-None-Match on a subsequent request so the server can respond with a lightweight 304 Not Modified instead of resending the full body if nothing has changed -- both let caching happen outside the application, at the browser or CDN level.
app.get('/api/products', (req, res) => {
res.set('Cache-Control', 'public, max-age=300');
res.set('ETag', computeEtag(products));
if (req.headers['if-none-match'] === computeEtag(products)) {
return res.status(304).end();
}
res.json(products);
});
Real-world example
A public product-catalog API sets Cache-Control: public, max-age=300 on its GET endpoints, letting a CDN serve the vast majority of read traffic directly from edge locations without ever reaching the origin server.
Common follow-ups: What's the difference between the 'public' and 'private' Cache-Control directives, and when should an API use 'private' or 'no-store' instead?;How does an ETag differ from a Last-Modified header for conditional requests?
HTTP & HTTPS Modules;Performance Optimization & Profiling
How would you implement a Least Recently Used (LRU) cache in Node.js, and why is eviction necessary for an in-memory cache?
Intermediate
An LRU cache has a fixed maximum size and, once full, evicts the entry that hasn't been accessed for the longest time to make room for a new one -- this bounds memory usage while keeping the most actively used data available, based on the assumption that recently accessed data is more likely to be accessed again soon.
const { LRUCache } = require('lru-cache');
const cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 10 });
cache.set('user:123', userData);
const cached = cache.get('user:123'); // moves this entry to 'most recently used'
Real-world example
A service caching computed search-result rankings for popular queries uses an LRU cache capped at 5,000 entries, so memory usage stays bounded regardless of how many unique queries are searched, automatically evicting rankings that have fallen out of recent popularity.
Common follow-ups: How does LRU eviction compare to other eviction policies like LFU (Least Frequently Used)?;What's the time complexity of get() and set() operations in a properly implemented LRU cache, and why does that matter at scale?
Memory Management & Garbage Collection;Performance Optimization & Profiling
What is Redis's EXPIRE and how do TTL-based keys interact with Redis's own memory eviction policies under memory pressure?
Advanced
EXPIRE (or the EX option on SET) sets a time-to-live on a Redis key, after which Redis automatically removes it -- but under memory pressure Redis can also proactively evict keys earlier than expiration according a configured eviction policy (like allkeys-lru, evicting the least recently used key regardless of TTL, or volatile-lru, only evicting keys with a TTL set), which matters because caching data with no TTL under an allkeys policy could have any key evicted at any time.
await redis.set('session:abc123', sessionData, { EX: 3600 });
# redis.conf: only evict keys that have a TTL set, oldest-accessed first
# maxmemory-policy volatile-lru
# maxmemory 2gb
Real-world example
A team configures their Redis instance with maxmemory-policy set to volatile-lru specifically because they store both cache data (with TTLs, safe to evict) and durable session data (without TTLs) in the same instance, ensuring memory pressure only ever evicts the cache entries.
Common follow-ups: What's the difference between 'noeviction' and 'allkeys-lru' policies, and what happens to write operations once Redis hits maxmemory under 'noeviction'?;Why might mixing cache data and durable data in the same Redis instance be considered risky regardless of eviction policy?
Caching with Redis;Databases & ORMs (MongoDB/Mongoose
SQL/Sequelize)
What is memoization, and how does it differ from a general-purpose external cache like Redis?
Intermediate
Memoization caches the return value of a specific pure function based on its input arguments, entirely within the process's memory and scoped to that function -- a code-level optimization for avoiding redundant computation, whereas an external cache like Redis is a general-purpose, application-wide (and often multi-process) data store used for a much broader range of caching needs beyond a single function call.
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
Real-world example
A pricing-calculation function performing an expensive deterministic computation based on a product's attributes is wrapped in a memoization helper, speeding up repeated calls for the same product configuration within a single process.
Common follow-ups: Why is memoization only safe for pure, deterministic functions with no side effects?;What's the risk of memoizing a function whose input arguments are complex objects, given JSON.stringify-based key generation?
Performance Optimization & Profiling;Functional Programming
What is the 'stale-while-revalidate' caching strategy, and how would you implement it for an API response?
Advanced
Stale-while-revalidate immediately returns the currently cached value (even past its ideal freshness window) to keep response latency low, while asynchronously triggering a background refresh for the next request -- trading a small amount of staleness for consistently fast response times.
async function getWithSWR(key, fetchFn, ttl = 60000) {
const cached = swrCache.get(key);
if (cached) {
if (Date.now() - cached.timestamp > ttl) {
fetchFn().then(fresh => swrCache.set(key, { value: fresh, timestamp: Date.now() }));
}
return cached.value;
}
const fresh = await fetchFn();
swrCache.set(key, { value: fresh, timestamp: Date.now() });
return fresh;
}
Real-world example
A dashboard displaying slowly-changing analytics metrics uses stale-while-revalidate so every page load feels instant, with the underlying data refreshing transparently in the background rather than making any user's request wait for recalculation.
Common follow-ups: What's the risk of this pattern for data where even brief staleness is unacceptable, like real-time account balances?;How does the HTTP Cache-Control 'stale-while-revalidate' directive implement this same idea at the HTTP layer?
HTTP & HTTPS Modules;Performance Optimization & Profiling
How would you cache the result of a database query in Node.js while ensuring the cache is properly invalidated when the underlying data changes?
Intermediate
The cache-aside pattern is combined with an explicit invalidation step triggered by any write to the same data -- whenever a record is updated or deleted, the corresponding cache key(s) are deleted immediately rather than waiting for TTL expiration, so the next read repopulates the cache with fresh data.
async function updateUser(id, data) {
await db.users.update(id, data);
await redis.del(`user:${id}`);
}
async function getUser(id) {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.users.findById(id);
await redis.set(`user:${id}`, JSON.stringify(user), { EX: 3600 });
return user;
}
Real-world example
A user-profile service deletes a user's cache entry every time their profile is updated, ensuring subsequent reads always reflect the latest data immediately rather than serving a stale cached profile for up to an hour.
Common follow-ups: What happens if a cache invalidation call itself fails after the database write already succeeded?;How do you handle invalidating a cache key derived from multiple underlying records, like a cached list of a user's orders?
Databases & ORMs (MongoDB/Mongoose
SQL/Sequelize);Error Handling