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

  1. Only call release() after a successful acquire() (or inside the `with` block after successful entry).
  2. Guard against double-release by tracking ownership yourself, or rely on the context manager which releases once.
  3. 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

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


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)