Module A-4·12 min read

The Redlock algorithm step by step across N independent Redis instances, what it guarantees under bounded clock drift, what Martin Kleppmann's critique gets right, fencing tokens as the correct complement, and implementation with redlock-node.

Written by Jatin Jain Saraf · Architect of SupraScan

A-4 — Redlock: The Algorithm, Its Guarantees, and Its Critics

Who this module is for: You use a single Redis instance for distributed locking but want the lock to survive a Redis node failure. Redlock is Redis's multi-instance distributed lock algorithm. This module covers the algorithm step-by-step, what it actually guarantees, Martin Kleppmann's critique (the most important distributed systems analysis of Redis locking), and when Redlock is and is not the right tool.


The Problem with Single-Instance Locking

A single-instance Redis lock has one critical failure mode: if the Redis master fails after granting a lock but before the lock holder releases it, and a failover promotes a replica, the replica does not have the lock (replication is asynchronous — the SET NX may not have replicated before the primary failed). The new primary issues the lock to a new client. Now two clients hold the same lock simultaneously.

Redlock addresses this by requiring a lock to be acquired on a majority of independent Redis instances.


The Redlock Algorithm

Redlock requires an odd number of independent Redis instances — typically 5. "Independent" means separate machines with no replication between them. A failure of any minority (< N/2) of instances does not affect lock correctness.

Step-by-Step

N = 5 independent Redis instances
Validity time = 30 seconds (desired lock TTL)
 
1. Record the start time: T1 = current time in milliseconds
 
2. For each of the N instances, attempt to acquire the lock:
   SET lock:resource {uuid} NX PX {validity_ms}
   Use a small per-instance timeout (e.g., 5–50ms) to avoid blocking if an instance is down.
 
3. Count successful acquisitions. Calculate elapsed time: T2 = current time
   Elapsed = T2 - T1
 
4. The lock is acquired if:
   a. Acquired on at least (N/2 + 1) = 3 of 5 instances (quorum)
   b. Remaining validity time > 0: (validity_ms - elapsed) > 0
 
5. If the lock is acquired:
   The "actual TTL" = validity_ms - elapsed - clock_drift_factor
   The holder can use the resource for at most this remaining time.
 
6. If the lock was NOT acquired (fewer than quorum, or time expired):
   Release the lock on all instances where it was acquired (to clean up).
   Wait a random backoff time before retrying.

Why Quorum?

If 3 of 5 instances grant the lock, no other client can simultaneously get quorum — at best, they see 2 instances (the 2 that failed or are unreachable). 2 < 3, so they cannot acquire the lock. The majority ensures safety.

Why Deduct Elapsed Time?

The lock is set on each instance with the full validity_ms. But by the time the algorithm finishes acquiring on all instances, some time has passed. The actual safe window for holding the lock is reduced by this elapsed time plus a small clock drift factor.


Implementation: redlock-node

npm install redlock
import Redlock from 'redlock';
import Redis from 'ioredis';
 
// 5 independent Redis instances
const clients = [
  new Redis({ host: 'redis-1.internal', port: 6379 }),
  new Redis({ host: 'redis-2.internal', port: 6379 }),
  new Redis({ host: 'redis-3.internal', port: 6379 }),
  new Redis({ host: 'redis-4.internal', port: 6379 }),
  new Redis({ host: 'redis-5.internal', port: 6379 }),
];
 
const redlock = new Redlock(clients, {
  driftFactor: 0.01,        // 1% clock drift factor
  retryCount: 3,            // retry attempts
  retryDelay: 200,          // ms between retries
  retryJitter: 100,         // random jitter on retry
  automaticExtensionThreshold: 500,  // extend lock when < 500ms remaining
});
 
// Acquire lock for 10 seconds
async function processPayment(paymentId: string) {
  let lock;
  try {
    lock = await redlock.acquire([`lock:payment:${paymentId}`], 10000);
    // Lock held — safe to execute
    await executePaymentLogic(paymentId);
  } catch (err) {
    if (err instanceof Redlock.ExecutionError) {
      throw new Error('Could not acquire distributed lock — payment already in progress');
    }
    throw err;
  } finally {
    if (lock) {
      await lock.release();
    }
  }
}
 
// Using the lock extension (watchdog pattern)
async function processLongJob(jobId: string) {
  await redlock.using(
    [`lock:job:${jobId}`],
    30000,  // initial TTL
    async (signal) => {
      // redlock.using() automatically extends the lock every ~15s
      // signal.aborted === true if the lock could not be extended
      await doWork(jobId, signal);
    }
  );
}

What Redlock Guarantees

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.