redis/redis-py · warning · LockNotOwnedError

Cannot release a lock that's no longer owned

Error message

Cannot release a lock that's no longer owned

What it means

Raised as LockNotOwnedError (subclass of LockError) by Lock.do_release() when the Lua release script returns 0: the key in Redis is missing or its stored token does not match this lock's expected_token. The lock is not (or no longer) owned by this lock instance, so the delete is skipped to avoid releasing someone else's lock.

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 da03cdc7e8)

Solutions

  1. Make the lock timeout longer than your worst-case critical section, or extend() the lock before it expires.
  2. Keep critical sections short and bounded.
  3. If you expect this (e.g., best-effort locking), catch LockNotOwnedError and treat as already-released.
  4. Investigate out-of-band key deletion if it recurs unexpectedly.

Example fix

// before
lock = Lock(redis, 'k', timeout=2)
await lock.acquire()
await long_task_taking_5s()  # TTL expires
await lock.release()  # LockNotOwnedError
// after
lock = Lock(redis, 'k', timeout=30)
await lock.acquire()
await long_task_taking_5s()
await lock.release()
Defensive patterns

Strategy: try-catch

Validate before calling

async def release_if_still_owned(lock):
    if await lock.owned():
        await lock.release()
    # else: TTL expired / lost; nothing to release

Type guard

async def lock_still_owned(lock) -> bool:
    return await lock.owned()

Try / catch

from redis.exceptions import LockNotOwnedError
try:
    await lock.release()
except LockNotOwnedError:
    # lock TTL expired or lost; treat as released
    ...

Prevention

When it happens

Trigger: The lock TTL expired before release() was called (owner crashed or was slow); another owner already acquired after expiry; the key was deleted out-of-band; a token mismatch due to thread_local confusion. Fires at lock.py:293-294.

Common situations: Critical section took longer than the lock timeout; the owning process restarted; someone manually DEL'd the key; double-release across tasks after the TTL lapsed.

Related errors


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