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 .