redis/redis-py · warning · LockNotOwnedError
Cannot release a lock that's no longer owned
Error message
Cannot release a lock that's no longer owned
What it means
Raised as LockNotOwnedError (subclass of LockError) by Lock.do_release() when the Lua release script returns 0: the key in Redis is missing or its stored token does not match this lock's expected_token. The lock is not (or no longer) owned by this lock instance, so the delete is skipped to avoid releasing someone else's lock.
Source
Thrown at redis/asyncio/lock.py:294
"Cannot release a lock that's not owned or is already unlocked.",
lock_name=self.name,
)
try:
await self.do_release(expected_token)
except LockNotOwnedError:
# Lock doesn't exist in Redis, safe to clear token
self.local.token = None
raise
# Only clear token after successful release
self.local.token = None
async def do_release(self, expected_token: bytes) -> None:
if not bool(
await self.lua_release(
keys=[self.name], args=[expected_token], client=self.redis
)
):
raise LockNotOwnedError("Cannot release a lock that's no longer owned")
def extend(
self, additional_time: Number, replace_ttl: bool = False
) -> Awaitable[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")
if self.timeout is None:
raise LockError("Cannot extend a lock with no timeout")View on GitHub (pinned to da03cdc7e8)
Solutions
- Make the lock timeout longer than your worst-case critical section, or extend() the lock before it expires.
- Keep critical sections short and bounded.
- If you expect this (e.g., best-effort locking), catch LockNotOwnedError and treat as already-released.
- Investigate out-of-band key deletion if it recurs unexpectedly.
Example fix
// before lock = Lock(redis, 'k', timeout=2) await lock.acquire() await long_task_taking_5s() # TTL expires await lock.release() # LockNotOwnedError // after lock = Lock(redis, 'k', timeout=30) await lock.acquire() await long_task_taking_5s() await lock.release()
Defensive patterns
Strategy: try-catch
Validate before calling
async def release_if_still_owned(lock):
if await lock.owned():
await lock.release()
# else: TTL expired / lost; nothing to release Type guard
async def lock_still_owned(lock) -> bool:
return await lock.owned() Try / catch
from redis.exceptions import LockNotOwnedError
try:
await lock.release()
except LockNotOwnedError:
# lock TTL expired or lost; treat as released
... Prevention
- Set timeout > worst-case critical section duration.
- extend() the lock before its TTL expires.
- Catch LockNotOwnedError for best-effort locking.
When it happens
Trigger: The lock TTL expired before release() was called (owner crashed or was slow); another owner already acquired after expiry; the key was deleted out-of-band; a token mismatch due to thread_local confusion. Fires at lock.py:293-294.
Common situations: Critical section took longer than the lock timeout; the owning process restarted; someone manually DEL'd the key; double-release across tasks after the TTL lapsed.
Related errors
- Cannot extend a lock that's no longer owned
- Cannot release a lock that's not owned or is already unlocke
- Cannot extend an unlocked lock
- Cannot reacquire an unlocked lock
- Unable to acquire lock within the time specified
AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04).
Data as JSON: /data/errors/2664c1c275c9c338.json.
Report an issue: GitHub.