Course contentsShow
← Node.js
Lesson 1349 of 1,98012. Caching Strategies and Application SpeedFree lesson

Distributed Locking with Cache Priming

You will prevent redundant background tasks by implementing distributed mutexes.

In this lesson, you will learn how to use distributed locks to ensure that only one Node.js instance performs a heavy cache priming operation, preventing "cache stampedes."

The Problem: Thundering Herds

When a frequently requested cache key expires, multiple incoming requests may simultaneously detect the "cache miss." If your application triggers a background task to recompute and prime the cache every time this happens, your database will be hammered by redundant, identical heavy queries. In a distributed environment, you need a way to coordinate these instances so only the first one to arrive performs the work while the others wait or serve stale data.

Distributed Locking for Synchronization

A distributed mutex (Mutual Exclusion) allows you to gate-keep the cache priming logic. By using an atomic operation in a store like Redis (such as SET key value NX PX 5000), you can guarantee that only one instance acquires the "lock" for a specific resource.

Here is how you implement this pattern in Node.js:

async function getOrPrimeCache(key) {
  let data = await cache.get(key);
  
  if (!data) {
    // Attempt to acquire a lock for this specific key
    const lockAcquired = await redis.set(`lock:${key}`, 'locked', 'NX', 'PX', 10000);
    
    if (lockAcquired) {
      // I am the chosen one: perform the expensive operation
      data = await db.fetchExpensiveData(key);
      await cache.set(key, data, 'EX', 3600);
      await redis.del(`lock:${key}`);
    } else {
      // Wait a moment and retry, or return a stale value/fallback
      await sleep(500);
      return getOrPrimeCache(key); 
    }
  }
  return data;
}

Why it works

The NX (Not Exists) flag ensures the lock is only created if it doesn't already exist. The PX (Expiration) ensures that if the process crashes mid-task, the lock will automatically expire rather than deadlocking your entire system. By holding the other requests at the gate, you protect your downstream services from redundant traffic and ensure your cache is primed efficiently by exactly one instance.

Key Takeaway: Use atomic distributed locks to serialize cache priming tasks, ensuring only one instance incurs the cost of a cache miss while others wait for the result.