Distributed Lock with Redis - Correctness & Risks

A distributed lock implemented with Redis (e.g. SET key value NX PX ttl) can coordinate multiple processes so only one holds the lock at a time. Correctness requires a unique value per client, a finite TTL to avoid deadlock, and safe release (only the holder deletes the key). This article explains the basic pattern, Lua for atomic release, and risks (clock skew, single point of failure, replication lag).

Overview

  • Acquire: SET lock_key unique_value NX PX ttl_ms. Only one client succeeds; others get nil. The value must be unique per client (e.g. UUID) so that only the holder can release.
  • Release: Delete the key only if the value matches (avoid releasing someone else’s lock). In Redis this is best done with a Lua script so GET + DEL are atomic.
  • TTL: Always set a TTL so that if the client crashes, the lock is eventually released. Make the TTL long enough for the critical section; consider refresh (watchdog) for long operations.
  • Risks: Single Redis node failure (use Redlock or accept limited guarantee), replication lag (replica can grant the same lock after failover), and clock skew if you use time-based logic.

Example

Example 1: Acquire and release (single node)

Redis
-- Acquire (NX = set if not exists; PX = expiry in ms)
SET resource:lock "uuid-from-client-123" NX PX 10000

-- Release: only if value still ours (Lua for atomicity)
-- EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 resource:lock uuid-from-client-123
  • If another client overwrote the key (e.g. after your TTL and they acquired), the Lua script would not delete (value mismatch). So you never release another client’s lock.

Example 2: Java with unique value and Lua release

Java
String lockKey = "order:123:lock";
String token = UUID.randomUUID().toString();
Boolean ok = redisTemplate.opsForValue().setIfAbsent(lockKey, token, Duration.ofSeconds(10));
if (Boolean.TRUE.equals(ok)) {
    try {
        doWork();
    } finally {
        redisTemplate.execute(RELEASE_SCRIPT, List.of(lockKey), token);
    }
}
  • setIfAbsent = SET NX; TTL in seconds. Release script compares value and deletes only if it matches.

Example 3: Why not just DEL

Redis
-- Client A acquires, holds lock, then is paused (GC, network) longer than TTL
-- Client B acquires same key (A's key expired)
-- Client A resumes and does DEL → releases B's lock (wrong)
  • So release must be conditional on value; Lua ensures no one else’s lock is removed.

Core Mechanism / Behavior

  • NX: Set only if key does not exist; used for “acquire once.”
  • PX / EX: Expiry in milliseconds or seconds; Redis will delete the key when time is up, so the lock is automatically released even if the client crashes.
  • Lua: Redis runs a script atomically; the script can read and then delete only when the value matches, so release is safe under concurrency.
StepAction
AcquireSET key unique_value NX PX ttl_ms
WorkDo critical section; optionally extend TTL (watchdog)
ReleaseLua: if GET(key)==my_value then DEL(key)

Key Rules

  • Use a unique value per client (UUID) and release only when value matches (Lua GET+DEL). Never release without checking ownership.
  • Always set a TTL to avoid permanent lock after client crash. Choose TTL longer than max expected critical section; for long work, consider refreshing TTL in a background thread (watchdog) or splitting work into shorter steps.
  • Understand Redis’s single-node and replication semantics: failover can lead to two holders if you rely on one node. Redlock (multiple nodes) improves safety but adds complexity; use only when you need it and understand its limitations.

What's Next

See Distributed Lock Options for Redlock and other backends. See Cache-Aside and Expiration for TTL and key design. See Rate Limiting for time-window patterns that may use similar primitives.