redis/redis-py · warning · LockError

Cannot extend an unlocked lock

Error message

Cannot extend an unlocked lock

What it means

Raised as LockError by Lock.extend() when self.local.token is None: there is no ownership token to extend. extend() only makes sense on a lock that has been acquired; without a token the Lua extend script cannot run.

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

Solutions

  1. Only call extend() between a successful acquire() and release(), within the same task (or use thread_local=False).
  2. Guard the watchdog loop to stop extending once the token is cleared/released.
  3. Use owned() to check ownership before extending if unsure.

Example fix

// before
lock = Lock(redis, 'k', timeout=10)
lock.extend(10)  # before acquire -> LockError
// after
await lock.acquire()
lock.extend(10)
...
Defensive patterns

Strategy: validation

Validate before calling

def can_extend(lock) -> bool:
    return lock.local.token is not None and lock.timeout is not None

if can_extend(lock):
    lock.extend(10)

Type guard

def lock_holds_token(lock) -> bool:
    return getattr(getattr(lock, 'local', None), 'token', None) is not None

Try / catch

from redis.exceptions import LockError
try:
    lock.extend(10)
except LockError as e:
    if 'unlocked' in str(e):
        # not acquired (or already released); skip extend
        ...

Prevention

When it happens

Trigger: Calling lock.extend(...) before acquire() succeeded, or after release() has already cleared the token. Fires at lock.py:309-310.

Common situations: Extending in a watchdog/keepalive loop that starts before acquisition completes; extending after an early release; extending from a different task under thread_local=True.

Related errors


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