Python Distributed Locks — Core Concepts
Why Distributed Locks?
When multiple processes or servers access a shared resource, you need coordination. A regular threading.Lock only works within a single process. A distributed lock works across processes, machines, and data centers.
Common use cases:
- Prevent duplicate processing — only one worker processes a given job
- Mutual exclusion — only one process runs a migration or cron job at a time
- Resource protection — limit concurrent access to a rate-limited API
- Leader election — one server becomes the coordinator while others wait
Redis-Based Locks
Redis is the most popular choice for distributed locks in Python because it’s fast, widely deployed, and supports atomic operations.
Basic Pattern with redis-py
The redis-py library includes a built-in lock:
import redis
r = redis.Redis()
# Acquire a lock with 30-second timeout
lock = r.lock('my-resource-lock', timeout=30)
if lock.acquire(blocking=True, blocking_timeout=10):
try:
# Critical section — only one process runs this
process_resource()
finally:
lock.release()
Under the hood, this uses SET key value NX EX timeout — an atomic set-if-not-exists with an expiration. The value is a unique token so only the lock holder can release it.
Key Parameters
timeout— how long the lock lives. After this, it auto-expires even if not released. Prevents deadlocks from crashed processes.blocking_timeout— how long to wait to acquire the lock. IfNone, wait forever.sleep— polling interval while waiting for the lock. Default is 0.1 seconds.
Database Advisory Locks
PostgreSQL and MySQL offer advisory locks — lightweight locks managed by the database:
# PostgreSQL advisory lock with SQLAlchemy
from sqlalchemy import text
def with_advisory_lock(session, lock_id, callback):
# pg_advisory_lock blocks until acquired
session.execute(text(
f"SELECT pg_advisory_lock({lock_id})"
))
try:
return callback()
finally:
session.execute(text(
f"SELECT pg_advisory_unlock({lock_id})"
))
Advisory locks are free (no table rows created), fast, and automatically released when the database connection closes. They’re a good choice when you’re already using PostgreSQL and don’t want to add Redis just for locking.
Lock Safety
The Timeout Dilemma
Set the timeout too short: the lock expires while you’re still working, and another process barges in. Set it too long: a crashed process blocks everyone for the full timeout duration.
There’s no perfect answer. The practical approach:
- Set a generous timeout (2-3x your expected processing time)
- Extend the lock periodically if the operation is still running
- Monitor for timeout-related issues
Fencing Tokens
Even with proper timeouts, there’s a subtle race condition. Process A acquires the lock, gets delayed (GC pause, network issue), the lock expires, Process B acquires the lock, and now both think they hold it.
Fencing tokens solve this: each lock acquisition gets a monotonically increasing token. The protected resource rejects requests with tokens older than the last one it saw.
When Not to Use Distributed Locks
- Idempotent operations — if running twice is harmless, you don’t need a lock
- Database transactions — if the contention is purely within a database, use
SELECT FOR UPDATEinstead - Single-process systems — use
threading.Lockorasyncio.Lock
Common Misconception
“Distributed locks guarantee mutual exclusion.” They don’t — at least, not with single-node Redis. Network partitions, clock skew, and Redis failovers can all cause two processes to hold the same lock simultaneously. Distributed locks provide mutual exclusion most of the time, which is sufficient for most applications. For stronger guarantees, you need consensus protocols (Redlock, ZooKeeper, etcd).
One thing to remember: Always use lock timeouts (auto-expiry) in distributed locks. A lock without a timeout is a deadlock waiting to happen — one crashed process will lock out everyone else permanently.
See Also
- Python Dead Letter Queues What happens to messages that can't be delivered — and why Python systems need a lost-and-found box.
- Python Delayed Task Execution How Python programs schedule tasks to run later — like setting an alarm for your code.
- Python Fan Out Fan In Pattern How Python splits big jobs into small pieces, runs them all at once, then puts the results back together.
- Python Message Deduplication Why computer messages sometimes get delivered twice — and how Python stops them from doing double damage.
- Python Priority Queue Patterns Why some tasks cut the line in Python — and how priority queues decide who goes first.