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.
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 & RegisterDiscussion
0Join the discussion