redis/redis-py · warning · LockNotOwnedError
Cannot extend a lock that's no longer owned
Error message
Cannot extend a lock that's no longer owned
What it means
Raised as LockNotOwnedError (subclass of LockError) by Lock.do_extend() when the Lua extend script returns 0: the key is missing, the stored token does not match this lock's token, or the key's TTL is negative (already expired). The extend is skipped to avoid touching a lock this instance no longer owns.
Source
Thrown at redis/asyncio/lock.py:324
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")
if self.timeout is None:
raise LockError("Cannot extend a lock with no timeout")
return self.do_extend(additional_time, replace_ttl)
async def do_extend(self, additional_time, replace_ttl) -> Literal[True]:
additional_time = int(additional_time * 1000)
if not bool(
await self.lua_extend(
keys=[self.name],
args=[self.local.token, additional_time, replace_ttl and "1" or "0"],
client=self.redis,
)
):
raise LockNotOwnedError("Cannot extend a lock that's no longer owned")
return True
def reacquire(self) -> Awaitable[Literal[True]]:
"""
Resets a TTL of an already acquired lock back to a timeout value.
"""
if self.local.token is None:
raise LockError("Cannot reacquire an unlocked lock")
if self.timeout is None:
raise LockError("Cannot reacquire a lock with no timeout")
return self.do_reacquire()
async def do_reacquire(self) -> Literal[True]:
timeout = int(self.timeout * 1000)
if not bool(
await self.lua_reacquire(
keys=[self.name], args=[self.local.token, timeout], client=self.redis
)View on GitHub (pinned to da03cdc7e8)
Solutions
- Extend early and often, well before the TTL expires (e.g., extend at TTL/3 intervals).
- Use a longer base timeout as a safety margin.
- Catch LockNotOwnedError in the watchdog and treat it as 'lock lost' (abort the critical section).
- Ensure single-owner semantics (thread_local setting matches your task model).
Example fix
// before
lock = Lock(redis, 'k', timeout=5)
await lock.acquire()
await asyncio.sleep(6) # TTL expired
lock.extend(5) # LockNotOwnedError
// after
lock = Lock(redis, 'k', timeout=10)
await lock.acquire()
for _ in range(work_units):
lock.extend(10) # renew before expiry
await step() Defensive patterns
Strategy: try-catch
Validate before calling
async def safe_extend(lock, extra):
if not await lock.owned():
return False # lock lost; do not attempt extend
try:
return await lock.extend(extra)
except Exception:
return False Type guard
async def lock_still_owned(lock) -> bool:
return await lock.owned() Try / catch
from redis.exceptions import LockNotOwnedError
try:
lock.extend(10)
except LockNotOwnedError:
# TTL expired / lost; abort the critical section
raise CriticalSectionAborted Prevention
- Extend well before the TTL expires (e.g., at TTL/3).
- Use a longer base timeout as a safety margin.
- Catch LockNotOwnedError to detect a lost lock.
- Keep single-owner semantics consistent with thread_local.
When it happens
Trigger: The lock TTL expired before extend() was called; another owner re-acquired after expiry; token mismatch (thread_local confusion); key deleted out-of-band. Fires at lock.py:323-324.
Common situations: A watchdog extending too late (after expiry); critical section overran the TTL; ownership confusion across tasks.
Related errors
- Cannot release a lock that's no longer owned
- Cannot extend an unlocked lock
- Cannot release a lock that's not owned or is already unlocke
- Cannot extend a lock with no timeout
- Cannot reacquire an unlocked lock
AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04).
Data as JSON: /data/errors/d27f31b0ce580e68.json.
Report an issue: GitHub.