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

do_release runs the Lua release script that checks the stored token; if the key is gone (TTL expired) or the token differs, it returns 0 and LockNotOwnedError is raised. This is server-side confirmation the lock is no longer yours, most commonly because its TTL expired while you were still in the critical section.

Solutions

  1. Ensure critical sections finish well within timeout, or extend() the lock proactively before it expires.
  2. Treat LockNotOwnedError as 'lock expired, the work may be unsafe' and abort/log.
  3. Use a longer timeout or a renewal task.
  4. Never manually delete lock keys.
Defensive patterns

Strategy: try-catch

Try / catch

from redis.exceptions import LockNotOwnedError
try:
    await lock.release()
except LockNotOwnedError:
    # TTL expired mid-section: work may be unsafe, abort/log
    ...

Prevention

When it happens

Trigger: Calling release() after the lock's timeout/TTL expired; the key was deleted or overwritten by another client; token mismatch.

Common situations: Critical section longer than timeout; manual del of the lock key; another holder overwrote it after expiry.

Related errors


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

Appendix: source

Thrown at redis/asyncio/lock.py:294

                "Cannot release a lock that's not owned or is already unlocked.",
                lock_name=self.name,
            )
        try:
            await self.do_release(expected_token)
        except LockNotOwnedError:
            # Lock doesn't exist in Redis, safe to clear token
            self.local.token = None
            raise
        # Only clear token after successful release
        self.local.token = None

    async def do_release(self, expected_token: bytes) -> None:
        if not bool(
            await self.lua_release(
                keys=[self.name], args=[expected_token], client=self.redis
            )
        ):
            raise LockNotOwnedError("Cannot release a lock that's no longer owned")

    def extend(
        self, additional_time: Number, replace_ttl: bool = False
    ) -> Awaitable[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:
            raise LockError("Cannot extend an unlocked lock")
        if self.timeout is None:
            raise LockError("Cannot extend a lock with no timeout")

View on GitHub (pinned to 6a6b581b48)