redis/redis-py · error · LockNotOwnedError

Cannot extend a lock that's no longer owned

Error message

Cannot extend a lock that's no longer owned

What it means

do_extend runs the Lua extend script; if the key is gone/TTL-expired, the token differs, or the current PTTL is negative, the script returns 0 and LockNotOwnedError is raised. The lock expired before the extend request reached the server.

Solutions

  1. Renew with margin before expiry (e.g. at one-third of the TTL).
  2. Treat as expired (the work may be unsafe) and abort/log.
  3. Use a longer base timeout so renewal has room.
  4. Catch LockNotOwnedError and re-acquire fresh.
Defensive patterns

Strategy: try-catch

Try / catch

from redis.exceptions import LockNotOwnedError
try:
    await lock.extend(additional_time)
except LockNotOwnedError:
    # lock expired before extend: abort unsafe work
    ...

Prevention

When it happens

Trigger: Calling extend() after the lock TTL already expired; another client took over the key; the key was deleted between acquire and extend.

Common situations: Renewal interval longer than remaining TTL; GC/event-loop pauses pushing extend past expiry; network delay.

Related errors


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

Appendix: source

Thrown at redis/asyncio/lock.py:324

        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]]:
        """
        Resets a TTL of an already acquired lock back to a timeout value.
        """
        if self.local.token is None:
            raise LockError("Cannot reacquire an unlocked lock")
        if self.timeout is None:
            raise LockError("Cannot reacquire a lock with no timeout")
        return self.do_reacquire()

    async def do_reacquire(self) -> Literal[True]:
        timeout = int(self.timeout * 1000)
        if not bool(
            await self.lua_reacquire(
                keys=[self.name], args=[self.local.token, timeout], client=self.redis
            )

View on GitHub (pinned to 6a6b581b48)