Redis

Distributed Locking with Redis to Orchestrate Access for a Shared Resource

June 9, 2026
7 min read

When multiple servers compete to update the same record, send the same notification, or run the same scheduled job — race conditions happen. Distributed locks with Redis let you enforce mutual exclusion across any number of processes or machines without a dedicated coordination service.

The Problem: Race Conditions in Distributed Systems

In a single-process app, a mutex handles concurrency. In a distributed system with multiple servers, threads, or workers, you need a lock that all participants can see — stored in a shared location like Redis.

Common scenarios requiring distributed locks:

  • Preventing duplicate email sends when multiple workers pick up the same job
  • Ensuring only one server runs a scheduled cron job at a time
  • Serializing access to a third-party API with strict rate limits
  • Coordinating inventory deductions to prevent overselling

The SET NX EX Pattern

The foundation of Redis locking is a single atomic command: SET key value NX EX seconds. NX means "only set if Not eXists", and EX sets an expiry — preventing deadlocks if the lock holder crashes.

# Acquire lock (returns OK on success, nil on failure)
SET lock:inventory:item42 "worker-uuid-abc123" NX EX 30

# Check if lock is held
GET lock:inventory:item42   # "worker-uuid-abc123"

# Release lock (must verify owner before deleting)
# Use a Lua script for atomic check-and-delete:
EVAL "
  if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
  else
    return 0
  end
" 1 lock:inventory:item42 "worker-uuid-abc123"

The unique value (UUID) is critical — it ensures only the lock owner can release it, preventing a slow worker from accidentally releasing a lock that was re-acquired after its TTL expired.

Python Implementation

import redis
import uuid
import time

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

RELEASE_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
"""

def acquire_lock(resource, ttl_seconds=30, retry=3, retry_delay=0.1):
    lock_key = f"lock:{resource}"
    lock_value = str(uuid.uuid4())

    for attempt in range(retry):
        acquired = r.set(lock_key, lock_value, nx=True, ex=ttl_seconds)
        if acquired:
            return lock_key, lock_value
        time.sleep(retry_delay * (attempt + 1))  # backoff

    return None, None

def release_lock(lock_key, lock_value):
    r.eval(RELEASE_SCRIPT, 1, lock_key, lock_value)

# Usage
lock_key, lock_value = acquire_lock("inventory:item42", ttl_seconds=10)
if lock_key:
    try:
        # Critical section — only one worker runs this at a time
        update_inventory(42)
    finally:
        release_lock(lock_key, lock_value)
else:
    print("Could not acquire lock — another worker is processing")

The Redlock Algorithm

For stronger guarantees against Redis node failures, Redis creator Salvatore Sanfilippo proposed Redlock: acquire the lock on a majority of N independent Redis instances (typically 5). The lock is valid only if acquired on at least ⌊N/2⌋+1 instances within a time window.

  • Tolerates up to ⌊N/2⌋ Redis node failures
  • Uses the redlock-py or redlock (Node.js) libraries
  • Overkill for most applications — single-node locking is sufficient when Redis has persistence and replication

Lock TTL Best Practices

  • TTL must exceed max expected work time — if your critical section can take up to 5 seconds, set TTL to 15+ seconds
  • Keep critical sections short — long-held locks create contention; do minimal work inside the lock
  • Never extend locks without re-acquiring — a lock that expires while held is safer than a runaway lock
  • Log failed acquisitions — contention spikes indicate a bottleneck in your design

Key Takeaways

  • SET NX EX atomically acquires a lock with automatic expiry — no deadlocks
  • Store a unique value as the lock owner to safely release only your own lock
  • Use Lua scripts for atomic check-and-release operations
  • Retry with backoff when acquisition fails — don't spin-wait
  • Redlock provides stronger guarantees in multi-node setups but adds complexity