Distributed locks.

Basic lock (single instance)

import secrets

def acquire(key, ttl=30):
    token = secrets.token_hex(16)
    if redis.set(f"lock:{key}", token, nx=True, ex=ttl):
        return token
    return None

def release(key, token):
    lua = """
    if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('DEL', KEYS[1])
    end
    return 0
    """
    redis.eval(lua, 1, f"lock:{key}", token)

CAS release: only owner can release.

Usage

token = acquire("user:1:update", ttl=30)
if not token:
    raise BusyError()
try:
    # critical section
finally:
    release("user:1:update", token)

Context manager

from contextlib import contextmanager

@contextmanager
def redis_lock(key, ttl=30):
    token = acquire(key, ttl)
    if not token: raise RuntimeError("Lock busy")
    try:
        yield
    finally:
        release(key, token)

with redis_lock("user:1"):
    do_work()

With retry / blocking

def acquire_with_wait(key, ttl=30, wait=5, interval=0.1):
    deadline = time.time() + wait
    while time.time() < deadline:
        token = acquire(key, ttl)
        if token: return token
        time.sleep(interval)
    return None

Lock renewal (long task)

If task takes longer than TTL, renew periodically:

def renew(key, token, ttl=30):
    lua = """
    if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('EXPIRE', KEYS[1], ARGV[2])
    end
    return 0
    """
    return redis.eval(lua, 1, f"lock:{key}", token, ttl)

# In a thread:
while not done:
    renew(key, token, 30)
    time.sleep(10)

Redlock (multi-instance)

For higher safety with N independent Redis instances:

from redis.cluster import RedisCluster
from redlock import Redlock

dlm = Redlock([{"host": "r1"}, {"host": "r2"}, {"host": "r3"}])

lock = dlm.lock("res", 1000)  # ttl ms
if lock:
    try: ...
    finally: dlm.unlock(lock)

Acquires on majority of instances. Used by some for stricter guarantees.

⚠️ Martin Kleppmann critique: GC pause, network delays can break Redlock too. Use fencing tokens for true safety.

Fencing tokens

Lock returns monotonic counter. Resource verifies token.

def acquire_with_fence(key, ttl):
    token = secrets.token_hex(16)
    if redis.set(f"lock:{key}", token, nx=True, ex=ttl):
        fence = redis.incr(f"fence:{key}")
        return token, fence
    return None, None

# Resource (DB / external):
if request.fence_token > stored_token:
    stored_token = request.fence_token
    process()
else:
    reject()

When NOT to use Redis locks

  • Strong consistency required → use DB row locks, Zookeeper, etcd.
  • Critical correctness → assume locks can fail.

Lock vs idempotency

For most cases, design operations to be idempotent. Lock is fallback.

Common mistakes

  • DEL without owner check → release someone else’s lock.
  • Short TTL + slow task → lock expires, race.
  • Long TTL + crash → resource stuck waiting.
  • Redlock without fencing → still not safe in edge cases.
  • Using locks where idempotency would suffice.

Read this next

If you want my Lua-backed distributed lock, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .