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

Raised as LockNotOwnedError by Lock.do_extend() (lock.py:315) when the server-side LUA_EXTEND script returns 0. The script returns 0 when GET of the lock key does not match this client's token (the lock expired, was released, or was taken over by another client). Unlike the two precondition errors above, this fires after a Redis round-trip and indicates ownership was lost at the server.

Source

Thrown at redis/lock.py:315

        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", lock_name=self.name)
        if self.timeout is None:
            raise LockError("Cannot extend a lock with no timeout", lock_name=self.name)
        return self.do_extend(additional_time, replace_ttl)

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

    def reacquire(self) -> 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", lock_name=self.name)
        if self.timeout is None:
            raise LockError(
                "Cannot reacquire a lock with no timeout",
                lock_name=self.name,
            )
        return self.do_reacquire()

View on GitHub (pinned to 6a6b581b48)

Solutions

  1. Catch LockNotOwnedError specifically and re-acquire the lock before continuing critical work.
  2. Increase the base timeout or call extend() more frequently (well before the TTL expires).
  3. Check lock.owned() immediately before extend() and skip/handle gracefully if False.
  4. Review the work segment length so it completes well within the timeout window.

Example fix

# before
lock.extend(5)

# after
try:
    lock.extend(5)
except LockNotOwnedError:
    # lock expired — must re-acquire before doing more protected work
    if lock.acquire(blocking=True, blocking_timeout=10):
        do_critical_work()
    else:
        abort_work()
Defensive patterns

Strategy: try-catch

Validate before calling

# Pre-check ownership server-side before extending
if not lock.owned():
    # ownership already lost — re-acquire instead of extending
    raise RuntimeError('lock not owned server-side; re-acquire')
lock.extend(additional_time)

Type guard

from redis.lock import Lock

def still_owned(lock: Lock) -> bool:
    """True when the server still holds this client's token."""
    return lock.owned()

Try / catch

from redis.exceptions import LockNotOwnedError

try:
    lock.extend(additional_time)
except LockNotOwnedError:
    # TTL expired / taken over — must re-acquire before continuing protected work
    if not lock.acquire(blocking=True, blocking_timeout=10):
        raise RuntimeError('could not re-acquire expired lock')

Prevention

When it happens

Trigger: The lock's TTL elapsed before extend() was called (additional_time exceeded the remaining TTL); another process called release() or overwrote the key; a Redis FLUSHALL/EVICT removed the key; clock drift between client timing of extend and server-side pttl.

Common situations: Long-running jobs whose processing time exceeds the lock timeout and extend() is called too late; multiple workers contending for the same lock name; Redis under memory pressure evicting keys; mis-sizing additional_time relative to the base timeout.

Related errors


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