Skip to main content

Command Palette

Search for a command to run...

Distributed Locks: Redis Redlock and Alternatives

Published
10 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Distributed Locks: Redis Redlock and Alternatives for 2025-2026

Metadata

{
  "seo_title": "Distributed Locks: Redis Redlock & Alternatives Guide 2025",
  "meta_description": "Master distributed locking with Redis Redlock, etcd, and modern alternatives. TypeScript examples, pitfalls, and best practices for cloud-native systems in 2025.",
  "primary_keyword": "distributed locks",
  "secondary_keywords": [
    "Redis Redlock",
    "distributed locking patterns",
    "etcd distributed locks",
    "TypeScript distributed systems",
    "cloud-native locking",
    "consensus algorithms",
    "distributed coordination",
    "lock-free architectures"
  ],
  "tags": [
    "distributed-systems",
    "redis",
    "typescript",
    "cloud-native",
    "concurrency",
    "microservices",
    "system-design"
  ],
  "search_intent": "informational, technical implementation",
  "content_role": "technical guide with practical implementation examples"
}

Introduction

In distributed systems, coordinating access to shared resources across multiple nodes remains one of the most challenging problems engineers face. Whether you're preventing duplicate payment processing, ensuring single-instance job execution, or coordinating database migrations across a microservices architecture, distributed locks are essential primitives that enable safe concurrent operations.

As we move through 2025 and into 2026, the landscape of distributed locking has evolved significantly. Cloud-native architectures, serverless computing, and edge deployments have introduced new requirements and constraints that traditional locking mechanisms weren't designed to handle. This guide explores modern approaches to distributed locking, with practical TypeScript implementations and real-world considerations for production systems.

The Distributed Locking Problem

At its core, distributed locking solves the mutual exclusion problem across multiple processes or machines. Unlike single-machine locks (mutexes, semaphores), distributed locks must handle:

Network partitions: Nodes may lose connectivity temporarily, creating split-brain scenarios where multiple nodes believe they hold the lock.

Clock skew: Different machines have different system times, making time-based lock expiration unreliable.

Process failures: A node holding a lock may crash without releasing it, requiring automatic timeout mechanisms.

Performance requirements: Lock acquisition and release must be fast enough for high-throughput systems, often requiring sub-millisecond latency.

The classic example involves a payment processing system where multiple workers process queued transactions. Without proper locking, two workers might process the same payment simultaneously, resulting in duplicate charges—an unacceptable outcome in production systems.

Why 2025-2026 Is Different

The distributed systems landscape has transformed dramatically:

Serverless and ephemeral compute: AWS Lambda, Google Cloud Functions, and similar platforms create short-lived execution contexts where traditional lock management patterns break down. Functions may be killed mid-execution, and cold starts introduce unpredictable latency.

Multi-region by default: Modern applications deploy across multiple geographic regions for latency and resilience. This introduces significant network delays (100-300ms cross-region) that affect lock coordination.

Kubernetes-native patterns: Container orchestration has become standard, with operators and controllers requiring sophisticated coordination primitives that go beyond simple locks.

Edge computing: CDN edge functions and IoT devices push computation to network edges, creating new distributed coordination challenges with intermittent connectivity.

Observability requirements: Modern systems demand detailed telemetry, tracing, and debugging capabilities for lock contention and deadlock detection.

These changes mean that solutions designed in 2015-2020 often don't meet current requirements. We need locking mechanisms that are cloud-native, observable, and resilient to the failure modes of modern infrastructure.

Redis Redlock: The Classic Approach

Redis Redlock, proposed by Salvatore Sanfilippo in 2015, attempts to provide distributed locking using multiple independent Redis instances. The algorithm works by:

  1. Getting the current time in milliseconds
  2. Attempting to acquire the lock in all N Redis instances sequentially
  3. Considering the lock acquired only if locks were obtained in the majority of instances (N/2 + 1)
  4. Ensuring the total time to acquire locks is less than the lock validity time

Here's a modern TypeScript implementation using Redis 7.x features:

import { Redis } from 'ioredis';
import { randomBytes } from 'crypto';

interface RedlockConfig {
  retryCount: number;
  retryDelay: number;
  retryJitter: number;
  driftFactor: number;
}

class Redlock {
  private clients: Redis[];
  private config: RedlockConfig;
  private quorum: number;

  constructor(clients: Redis[], config: Partial<RedlockConfig> = {}) {
    this.clients = clients;
    this.quorum = Math.floor(clients.length / 2) + 1;
    this.config = {
      retryCount: 3,
      retryDelay: 200,
      retryJitter: 100,
      driftFactor: 0.01,
      ...config
    };
  }

  async acquire(
    resource: string,
    ttl: number
  ): Promise<{ value: string; validity: number } | null> {
    const value = randomBytes(20).toString('hex');

    for (let i = 0; i < this.config.retryCount; i++) {
      const start = Date.now();
      let locksAcquired = 0;

      // Try to acquire lock on all instances
      const results = await Promise.allSettled(
        this.clients.map(client =>
          client.set(resource, value, 'PX', ttl, 'NX')
        )
      );

      locksAcquired = results.filter(
        r => r.status === 'fulfilled' && r.value === 'OK'
      ).length;

      const elapsed = Date.now() - start;
      const drift = Math.floor(ttl * this.config.driftFactor) + 2;
      const validity = ttl - elapsed - drift;

      // Check if we acquired majority and have valid time remaining
      if (locksAcquired >= this.quorum && validity > 0) {
        return { value, validity };
      }

      // Release any acquired locks if we failed
      await this.release(resource, value);

      // Wait before retry with jitter
      if (i < this.config.retryCount - 1) {
        const delay = this.config.retryDelay + 
          Math.random() * this.config.retryJitter;
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }

    return null;
  }

  async release(resource: string, value: string): Promise<void> {
    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `;

    await Promise.allSettled(
      this.clients.map(client =>
        client.eval(script, 1, resource, value)
      )
    );
  }

  async extend(
    resource: string,
    value: string,
    ttl: number
  ): Promise<boolean> {
    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("pexpire", KEYS[1], ARGV[2])
      else
        return 0
      end
    `;

    const results = await Promise.allSettled(
      this.clients.map(client =>
        client.eval(script, 1, resource, value, ttl)
      )
    );

    const extended = results.filter(
      r => r.status === 'fulfilled' && r.value === 1
    ).length;

    return extended >= this.quorum;
  }
}

// Usage example
const redisClients = [
  new Redis({ host: 'redis1.example.com', port: 6379 }),
  new Redis({ host: 'redis2.example.com', port: 6379 }),
  new Redis({ host: 'redis3.example.com', port: 6379 }),
];

const redlock = new Redlock(redisClients);

async function processPayment(paymentId: string) {
  const lock = await redlock.acquire(`payment:${paymentId}`, 10000);

  if (!lock) {
    throw new Error('Could not acquire lock');
  }

  try {
    // Process payment logic here
    console.log('Processing payment', paymentId);

    // Extend lock if needed
    if (needMoreTime) {
      await redlock.extend(`payment:${paymentId}`, lock.value, 5000);
    }
  } finally {
    await redlock.release(`payment:${paymentId}`, lock.value);
  }
}

Modern Alternatives to Redlock

1. etcd Distributed Locks

etcd, the distributed key-value store backing Kubernetes, provides robust locking through its lease mechanism and MVCC (Multi-Version Concurrency Control):

import { Etcd3, Lease } from 'etcd3';

class EtcdLock {
  private client: Etcd3;
  private lease: Lease | null = null;

  constructor(endpoints: string[]) {
    this.client = new Etcd3({ hosts: endpoints });
  }

  async acquire(key: string, ttl: number): Promise<boolean> {
    this.lease = this.client.lease(ttl);
    await this.lease.grant();

    try {
      // Atomic compare-and-swap with lease
      const result = await this.client
        .if(key, 'Create', '==', 0)
        .then(this.client.put(key).value('locked').lease(this.lease))
        .else(this.client.get(key))
        .commit();

      return result.succeeded;
    } catch (error) {
      await this.lease.revoke();
      this.lease = null;
      throw error;
    }
  }

  async release(key: string): Promise<void> {
    if (this.lease) {
      await this.lease.revoke();
      this.lease = null;
    }
  }

  async keepAlive(): Promise<void> {
    if (this.lease) {
      await this.lease.keepaliveOnce();
    }
  }
}

Advantages: Strong consistency guarantees, built-in leader election, native Kubernetes integration.

Disadvantages: Higher latency than Redis, more complex operational requirements.

2. DynamoDB Conditional Writes

For AWS-native applications, DynamoDB provides atomic conditional writes that can implement distributed locks:

import { DynamoDBClient, PutItemCommand, DeleteItemCommand } from '@aws-sdk/client-dynamodb';

class DynamoDBLock {
  private client: DynamoDBClient;
  private tableName: string;

  constructor(tableName: string) {
    this.client = new DynamoDBClient({});
    this.tableName = tableName;
  }

  async acquire(lockId: string, ownerId: string, ttl: number): Promise<boolean> {
    const expiresAt = Date.now() + ttl;

    try {
      await this.client.send(new PutItemCommand({
        TableName: this.tableName,
        Item: {
          lockId: { S: lockId },
          ownerId: { S: ownerId },
          expiresAt: { N: expiresAt.toString() }
        },
        ConditionExpression: 'attribute_not_exists(lockId) OR expiresAt < :now',
        ExpressionAttributeValues: {
          ':now': { N: Date.now().toString() }
        }
      }));
      return true;
    } catch (error: any) {
      if (error.name === 'ConditionalCheckFailedException') {
        return false;
      }
      throw error;
    }
  }
}

3. PostgreSQL Advisory Locks

For systems already using PostgreSQL, advisory locks provide a simple, reliable solution:

import { Pool } from 'pg';

class PostgresLock {
  private pool: Pool;

  constructor(connectionString: string) {
    this.pool = new Pool({ connectionString });
  }

  async acquire(lockId: number): Promise<boolean> {
    const result = await this.pool.query(
      'SELECT pg_try_advisory_lock($1) as acquired',
      [lockId]
    );
    return result.rows[0].acquired;
  }

  async release(lockId: number): Promise<void> {
    await this.pool.query('SELECT pg_advisory_unlock($1)', [lockId]);
  }
}

Critical Pitfalls and How to Avoid Them

1. Clock Drift and Time-Based Expiration

Problem: Redlock relies on system clocks for TTL calculations. Clock drift can cause locks to expire prematurely or persist too long.

Solution: Use monotonic clocks where possible, implement clock skew detection, and add sufficient drift margins (typically 1-2% of TTL).

2. Lock Fencing Tokens

Problem: A slow client might hold a lock past expiration, then perform operations after another client acquires the lock.

Solution: Implement fencing tokens—monotonically increasing counters that resources check before accepting operations:

interface LockResult {
  token: number;
  value: string;
}

async function performOperation(lock: LockResult, resource: Resource) {
  // Resource validates token before accepting operation
  await resource.executeWithFencing(lock.token, () => {
    // Operation logic
  });
}

3. Network Partition Handling

Problem: During network partitions, multiple nodes may believe they hold the lock.

Solution: Implement lease-based locking with heartbeats, and design operations to be idempotent where possible.

4. Lock Contention and Thundering Herd

Problem: Many clients competing for the same lock can overwhelm the system.

Solution: Implement exponential backoff with jitter, use fair queuing mechanisms, or redesign to avoid hot locks.

Best Practices for 2025-2026

  1. Prefer lease-based locks: Use time-bounded leases with automatic renewal rather than indefinite locks.

  2. Implement comprehensive observability: Track lock acquisition time, hold duration, contention metrics, and timeout rates.

  3. Design for failure: Assume locks will fail and implement fallback strategies—circuit breakers, graceful degradation, or eventual consistency models.

  4. Use appropriate granularity: Fine-grained locks reduce contention but increase complexity. Balance based on your access patterns.

  5. Consider lock-free alternatives: For many use cases, optimistic concurrency control or CRDTs (Conflict-free Replicated Data Types) eliminate the need for locks entirely.

  6. Test partition scenarios: Use chaos engineering tools like Toxiproxy to simulate network failures and validate your locking behavior.

  7. Document lock hierarchies: Prevent deadlocks by establishing and enforcing a consistent lock acquisition order.

Frequently Asked Questions

Q: Should I use Redlock in production?

A: Redlock has been controversial since its introduction. For critical systems requiring strong consistency guarantees, prefer etcd or ZooKeeper. For less critical use cases where occasional race conditions are acceptable, Redlock with proper configuration can work. Always implement fencing tokens for safety.

Q: How do I choose between Redis, etcd, and database-based locks?

A: Consider your existing infrastructure. If you already run etcd (Kubernetes), use it. If you have PostgreSQL, advisory locks are simple and reliable. Redis is best for high-throughput, low-latency scenarios where eventual consistency is acceptable.

Q: What TTL should I set for distributed locks?

A: Set TTL to 2-3x your expected operation duration. Too short risks premature expiration; too long delays recovery from failures. Implement lock extension for long-running operations rather than using very long TTLs.

Q: How do I handle lock acquisition failures?

A: Implement retry logic with exponential backoff and jitter. After a reasonable number of retries (3-5), fail the operation and alert. Never retry indefinitely, as this can mask underlying issues.

Q: Can I use distributed locks in serverless functions?

A: Yes, but with caution. Serverless functions may be killed mid-execution, so always set conservative TTLs. Consider using DynamoDB or Redis with short TTLs (1-5 seconds) and design for idempotency.

Q: What's the performance overhead of distributed locks?

A: Expect 1-5ms latency for single-region Redis locks, 10-50ms for etcd, and 5-20ms for database advisory locks. Cross-region adds 100-300ms. Profile your specific setup under load.

Q: How do I debug distributed lock issues in production?

A: Implement structured logging with correlation IDs, track lock acquisition/release events, monitor lock hold times, and use distributed tracing (OpenTelemetry) to visualize lock contention across services.

Conclusion

Distributed locks remain a fundamental building block for coordinating operations in modern distributed systems. While Redis Redlock provides a pragmatic solution for many use cases, the landscape in 2025-2026 offers sophisticated alternatives better suited to cloud-native, multi-region, and serverless architectures.

The key is understanding your consistency requirements, failure modes, and operational constraints. For critical financial transactions or data integrity operations, invest in strongly consistent solutions like etcd. For high-throughput caching or rate limiting, Redis-based approaches offer excellent performance. For systems already using PostgreSQL or DynamoDB, leverage their native locking primitives.

Most importantly, design your systems to be resilient to lock failures. Implement idempotency, use fencing tokens, monitor lock behavior in production, and always have a fallback strategy. The best distributed lock is often the one you don't need—consider whether optimistic concurrency control, event sourcing, or CRDTs might eliminate the need for locks entirely.

As distributed systems continue to evolve, so too will our coordination primitives. Stay informed about emerging patterns, test thoroughly under realistic failure conditions, and always prioritize correctness over performance when dealing with critical shared resources.