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

  1. Release exactly once, ideally via the context manager (`with client.lock(...):`) which handles it automatically.
  2. Before a manual release(), check lock.owned() (or that self.local.token is set) to guard double-release.
  3. If crossing threads/processes, construct the Lock with thread_local=False and manage the token explicitly.
  4. 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).
  5. 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

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


AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04). Data as JSON: /data/errors/b3e9e023b419f988.json. Report an issue: GitHub.