redis/redis-py · error · LockError

Cannot extend an unlocked lock

Error message

Cannot extend an unlocked lock

What it means

extend() first checks the local token; if it is None the lock was never acquired (or already released) on this instance, so the client refuses to run the Lua extend and raises LockError before touching Redis.

Solutions

  1. Acquire before extend; guard with 'if await lock.owned()'.
  2. Use thread_local=False for cross-task renewal so the token is shared.
  3. Do not extend after release.

Example fix

# before
await lock.extend(10)  # before any acquire -> LockError
# after
if await lock.owned():
    await lock.extend(10)
Defensive patterns

Strategy: validation

Validate before calling

if await lock.owned():
    await lock.extend(additional_time)

Prevention

When it happens

Trigger: Calling extend(...) before acquire(), after release(), or from a different thread/task than the acquirer when thread_local=True.

Common situations: A renewal task started before acquisition; extending after release; cross-task token invisibility with the default thread_local=True.

Related errors


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

Appendix: source

Thrown at redis/asyncio/lock.py:310

            )
        ):
            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")
        return self.do_extend(additional_time, replace_ttl)

    async def do_extend(self, additional_time, replace_ttl) -> Literal[True]:
        additional_time = int(additional_time * 1000)
        if not bool(
            await self.lua_extend(
                keys=[self.name],
                args=[self.local.token, additional_time, replace_ttl and "1" or "0"],
                client=self.redis,
            )
        ):
            raise LockNotOwnedError("Cannot extend a lock that's no longer owned")
        return True

    def reacquire(self) -> Awaitable[Literal[True]]:
        """

View on GitHub (pinned to 6a6b581b48)