redis/redis-py · warning · LockError
Cannot release a lock that's not owned or is already unlocke
Error message
Cannot release a lock that's not owned or is already unlocked.
What it means
Raised as redis.exceptions.LockError by Lock.release() (redis/lock.py:271) when self.local.token is None — meaning this Lock instance never acquired the lock (acquire() was never called or returned False), or already released it. The token is the proof of ownership; without it there is nothing to release. Note this is the client-side guard; a separate LockNotOwnedError is raised by do_release if the token no longer matches the server value.
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 da03cdc7e8)
Solutions
- Release exactly once, ideally via the context manager (`with client.lock(...):`) which handles it automatically.
- Before a manual release(), check lock.owned() (or that self.local.token is set) to guard double-release.
- If crossing threads/processes, construct the Lock with thread_local=False and manage the token explicitly.
- Set raise_on_release_error=False on the Lock to log-and-continue instead of raising on the spurious release (the context manager already does this).
- Do not call release() inside an outer finally if the `with` block (or an inner finally) already releases.
Example fix
// before
lock = client.lock('x')
lock.release() # raises: never acquired
// after
lock = client.lock('x')
if lock.acquire():
try:
do_work()
finally:
lock.release()
# or, for the double-release footgun, guard explicitly:
if lock.local.token is not None:
lock.release() Defensive patterns
Strategy: validation
Validate before calling
lock = client.lock('x', thread_local=False)
if lock.local.token is not None:
lock.release()
# or use the context manager which releases exactly once:
# with client.lock('x') as lock: ... Type guard
def lock_is_held_locally(lock) -> bool:
return getattr(getattr(lock, 'local', None), 'token', None) is not None Try / catch
from redis.exceptions import LockError
try:
lock.release()
except LockError:
# already released / never acquired — benign in many flows
pass Prevention
- Prefer the context manager (`with client.lock(...):`) which releases exactly once.
- Guard manual release() with a check that the local token is set, or use lock.owned().
- For cross-thread/process release, construct the lock with thread_local=False and manage the token yourself.
- Avoid nested finally blocks that both release; set raise_on_release_error=False if a spurious release should be non-fatal.
- Never call release() after the `with` block has exited (it already released).
When it happens
Trigger: Calling lock.release() twice; calling release() on a Lock that was never acquired; calling release() after the context manager already exited (which releases once); calling release() when acquire() returned False (non-blocking miss).
Common situations: Double-release in overlapping finally blocks; releasing a lock from a different thread/process than the one that acquired it (thread_local=True default hides the token); releasing after __exit__ already released; releasing a lock whose `with` block raised LockError on enter (so it was never held).
Related errors
- Unable to acquire lock within the time specified
- Cannot reacquire a lock that's no longer owned
- Unable to acquire lock within the time specified
- Cannot release a lock that's not owned or is already unlocke
- Cannot release a lock that's no longer owned
AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04).
Data as JSON: /data/errors/b3e9e023b419f988.json.
Report an issue: GitHub.