redis/redis-py · error · LockNotOwnedError

Cannot release a lock that's no longer owned

Error message

Cannot release a lock that's no longer owned

What it means

Lock.do_release (redis/lock.py:282) raises LockNotOwnedError (a LockError subclass) when the server-side Lua release script returns falsy — the token stored at the lock key did not match the expected token. That means the lock was never held, already expired (timeout elapsed), or was stolen/re-acquired by another holder between acquire and release. Unlike error 438 (a client-side pre-check), this is the server confirming you no longer own the key.

Source

Thrown at redis/lock.py:282

    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]:
        """
        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:

View on GitHub (pinned to 6a6b581b48)

Solutions

  1. Set a lock timeout comfortably larger than your worst-case critical section, or call lock.extend() periodically for long work.
  2. Catch LockNotOwnedError on release and treat it as 'I may not have been the sole owner' — re-check/recover the protected resource.
  3. Use raise_on_release_error=True (default False in __exit__) only if losing ownership must be fatal; otherwise log it.
  4. Avoid manual del on the lock key; always release through the Lock API.

Example fix

// before
with Lock(r, 'task', timeout=5):
    long_running_task()  # may exceed 5s, lock expires
// after
from redis.exceptions import LockNotOwnedError
with Lock(r, 'task', timeout=60) as lock:
    while working:
        do_chunk(); lock.extend(60)
# and on release:
try:
    lock.release()
except LockNotOwnedError:
    log.warning('lock expired before release; verify protected state')
Defensive patterns

Strategy: try-catch

Validate before calling

def still_owned(lock) -> bool:
    return lock.owned()

Type guard

def lock_is_owned(lock) -> bool:
    return lock.owned()

Try / catch

from redis.exceptions import LockNotOwnedError
try:
    lock.release()
except LockNotOwnedError:
    log.warning('lock expired before release; re-verify protected resource')

Prevention

When it happens

Trigger: release() after the lock TTL expired (your critical section took longer than the lock timeout); releasing a lock whose key was overwritten; the token lost because of a failover where the key lived on a node that went down; releasing after manually deleting the key.

Common situations: Long-running work exceeding the lock timeout so the lock silently expired and another worker took it; no extend() calls for long sections; a failover losing the lock key; mixing manual DEL with the Lock abstraction.

Related errors


AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10). Data as JSON: /api/errors/8fa21cfed0055188. Report an issue: GitHub.