redis/redis-py · error · LockNotOwnedError
Cannot extend a lock that's no longer owned
Error message
Cannot extend a lock that's no longer owned
What it means
do_extend runs the Lua extend script; if the key is gone/TTL-expired, the token differs, or the current PTTL is negative, the script returns 0 and LockNotOwnedError is raised. The lock expired before the extend request reached the server.
Solutions
- Renew with margin before expiry (e.g. at one-third of the TTL).
- Treat as expired (the work may be unsafe) and abort/log.
- Use a longer base timeout so renewal has room.
- Catch LockNotOwnedError and re-acquire fresh.
Defensive patterns
Strategy: try-catch
Try / catch
from redis.exceptions import LockNotOwnedError
try:
await lock.extend(additional_time)
except LockNotOwnedError:
# lock expired before extend: abort unsafe work
... Prevention
- Renew at a fraction of the TTL so extend runs before expiry.
- Make critical sections abort-safe in case the lock is lost.
- Use a longer base timeout when renewal jitter is high.
When it happens
Trigger: Calling extend() after the lock TTL already expired; another client took over the key; the key was deleted between acquire and extend.
Common situations: Renewal interval longer than remaining TTL; GC/event-loop pauses pushing extend past expiry; network delay.
Related errors
- Cannot reacquire a lock that's no longer owned
- Cannot release a lock that's no longer owned
- Cannot extend an unlocked lock
- Cannot reacquire an unlocked lock
- Cannot release a lock that's not owned or is already…
AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10).
Data as JSON: /api/errors/d27f31b0ce580e68.
Report an issue: GitHub.
Appendix: 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 6a6b581b48)