redis/redis-py · error · LockError
Cannot release a lock that's not owned or is already…
Error message
Cannot release a lock that's not owned or is already unlocked.
What it means
Lock.release (redis/lock.py:271) raises LockError('Cannot release a lock that's not owned or is already unlocked.') when self.local.token is None — meaning release() was called before any successful acquire() on this thread/context, or release() was called twice (the first release clears the token). This is a client-side ownership check before the server-side Lua release runs.
Solutions
- Only call release() after a successful acquire() (or inside the `with` block after successful entry).
- Guard against double-release by tracking ownership yourself, or rely on the context manager which releases once.
- If releasing across threads, construct the Lock with thread_local=False so the token is shared.
Example fix
// before
lock.release() # in finally, even if acquire failed
// after
acquired = lock.acquire(blocking=False)
if acquired:
try:
do_work()
finally:
lock.release() Defensive patterns
Strategy: try-catch
Validate before calling
def release_if_owned(lock):
if getattr(lock.local, 'token', None) is not None:
lock.release() Type guard
def lock_is_locally_owned(lock) -> bool:
return getattr(lock.local, 'token', None) is not None Try / catch
from redis.exceptions import LockError
try:
lock.release()
except LockError:
pass # nothing to release Prevention
- Only release after a successful acquire; guard release in finally with an ownership flag.
- Avoid double-release; rely on the context manager for single release.
- Use thread_local=False if you must release from a different thread than the acquirer.
When it happens
Trigger: Calling lock.release() without having acquired the lock; calling release() twice (the second call sees token already None); using thread_local=True and calling release from a different thread than the one that acquired (the token is in thread-local storage); exiting the context manager twice.
Common situations: Releasing in a finally block when acquire failed; double-release in nested cleanup; cross-thread release with the default thread_local=True; a bug where release runs on an un-acquired lock instance.
Related errors
- Cannot extend a lock that's no longer owned
- Cannot extend a lock that's no longer owned
- Cannot extend a lock with no timeout
- Cannot extend an unlocked lock
- Cannot extend an unlocked lock
AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10).
Data as JSON: /api/errors/b3e9e023b419f988.
Report an issue: GitHub.
Appendix: source
Thrown at redis/lock.py:271
def owned(self) -> bool:
"""
Returns True if this key is locked by this lock, otherwise False.
"""
stored_token = self.redis.get(self.name)
# need to always compare bytes to bytes
# TODO: this can be simplified when the context manager is finished
if stored_token and not isinstance(stored_token, bytes):
encoder = self.redis.get_encoder()
stored_token = encoder.encode(stored_token)
return self.local.token is not None and stored_token == self.local.token
def release(self) -> None:
"""
Releases the already acquired lock
"""
expected_token = self.local.token
if expected_token is None:
raise LockError(
"Cannot release a lock that's not owned or is already unlocked.",
lock_name=self.name,
)
self.local.token = None
self.do_release(expected_token)
def do_release(self, expected_token: str) -> None:
if not bool(
self.lua_release(keys=[self.name], args=[expected_token], client=self.redis)
):
raise LockNotOwnedError(
"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]:View on GitHub (pinned to 6a6b581b48)