redis/redis-py · error · LockError
Cannot extend an unlocked lock
Error message
Cannot extend an unlocked lock
What it means
Raised by Lock.extend() when self.local.token is None, meaning the current thread has no recorded ownership token for this lock. The token is set only on a successful acquire() and cleared on release(), so extend() refuses to run before the lock is held or after it has been released. This is a client-side precondition check (LockError) that fires before any Redis round-trip.
Source
Thrown at redis/lock.py:301
"Cannot release a lock that's no longer owned",
lock_name=self.name,
)
def extend(
self, additional_time: Number, replace_ttl: bool = False
) -> Literal[True]:
"""
Adds more time to an already acquired lock.
``additional_time`` can be specified as an integer or a float, both
representing the number of seconds to add.
``replace_ttl`` if False (the default), add `additional_time` to
the lock's existing ttl. If True, replace the lock's ttl with
`additional_time`.
"""
if self.local.token is None:
raise LockError("Cannot extend an unlocked lock", lock_name=self.name)
if self.timeout is None:
raise LockError("Cannot extend a lock with no timeout", lock_name=self.name)
return self.do_extend(additional_time, replace_ttl)
def do_extend(self, additional_time: Number, replace_ttl: bool) -> Literal[True]:
additional_time = int(additional_time * 1000)
if not bool(
self.lua_extend(
keys=[self.name],
args=[self.local.token, additional_time, "1" if replace_ttl else "0"],
client=self.redis,
)
):
raise LockNotOwnedError(
"Cannot extend a lock that's no longer owned",
lock_name=self.name,
)
return TrueView on GitHub (pinned to 6a6b581b48)
Solutions
- Ensure lock.acquire() returns True before calling extend(); for non-blocking acquire, check the boolean return value.
- Use the context manager form (with lock:) so acquire/release are balanced automatically.
- Guard with lock.owned() before extending, and re-acquire if it returns False.
- If extending from a different thread than the one that acquired, construct the Lock with thread_local=False.
Example fix
# before
lock = client.lock('mylock', timeout=10)
lock.extend(5)
# after
lock = client.lock('mylock', timeout=10)
if lock.acquire():
try:
lock.extend(5)
finally:
lock.release() Defensive patterns
Strategy: validation
Validate before calling
# Validate ownership before extending
if lock.local.token is None or not lock.owned():
raise RuntimeError('cannot extend: lock not held by this thread')
lock.extend(additional_time) Type guard
from redis.lock import Lock
def is_held(lock: Lock) -> bool:
"""True only when the current thread holds the lock server-side."""
return lock.local.token is not None and lock.owned() Try / catch
from redis.exceptions import LockError
try:
lock.extend(additional_time)
except LockError as e:
if 'unlocked' in str(e):
# not held locally — re-acquire before retrying
if lock.acquire(blocking=True, blocking_timeout=5):
lock.extend(additional_time)
else:
raise Prevention
- Always pair acquire()/release() — prefer the 'with lock:' context manager.
- Check the boolean return of non-blocking acquire() before extend().
- Keep extend() calls on the same thread that called acquire(), or set thread_local=False.
When it happens
Trigger: Calling lock.extend(additional_time) before lock.acquire() has returned True; calling extend() after lock.release() (which sets self.local.token = None at lock.py:275); calling extend() from a worker thread different from the acquiring thread when thread_local=True (default), because the token lives in thread-local storage and is invisible to other threads.
Common situations: Forgetting to acquire before extending; an earlier acquire() that returned False (non-blocking) being silently ignored; extending inside an except/finally block that already released the lock; passing a Lock instance across threads without thread_local=False.
Related errors
- Cannot reacquire an unlocked lock
- Cannot extend a lock with no timeout
- Cannot reacquire a lock with no timeout
- Unable to acquire lock within the time specified
- Cannot release a lock that's not owned or is already unlocke
AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10).
Data as JSON: /api/errors/d05996e02bb11c7e.
Report an issue: GitHub.