redis/redis-py · error · LockError

Cannot extend a lock with no timeout

Error message

Cannot extend a lock with no timeout

What it means

Raised by Lock.extend() when self.timeout is None, i.e. the Lock was constructed without a timeout (a permanent lock). Extending works by adjusting an existing TTL server-side via the LUA_EXTEND script, which has no TTL to operate on for a permanent lock. This is a client-side precondition check (LockError) raised at lock.py:303.

Source

Thrown at redis/lock.py:303

            )

    def extend(
        self, additional_time: Number, replace_ttl: bool = False
    ) -> 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", 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]:

View on GitHub (pinned to 6a6b581b48)

Solutions

  1. Construct the Lock with a positive numeric timeout (seconds), e.g. Lock(client, 'name', timeout=30).
  2. If you need a permanent lock, do not call extend()/reacquire() — there is no TTL to refresh.
  3. Switch to reacquire()/extend() only after creating the lock with a finite timeout.

Example fix

# before
lock = client.lock('mylock', timeout=None)
lock.acquire()
lock.extend(10)  # raises

# after
lock = client.lock('mylock', timeout=30)
lock.acquire()
lock.extend(10)  # ok
Defensive patterns

Strategy: validation

Validate before calling

# Validate the lock has a TTL before extending
if lock.timeout is None:
    raise RuntimeError('cannot extend: lock has no timeout (permanent lock)')
lock.extend(additional_time)

Type guard

from redis.lock import Lock

def is_ttl_lock(lock: Lock) -> bool:
    """True when the lock was created with a finite timeout."""
    return lock.timeout is not None and lock.timeout > 0

Try / catch

from redis.exceptions import LockError

try:
    lock.extend(additional_time)
except LockError as e:
    if 'no timeout' in str(e):
        raise RuntimeError('recreate the Lock with a finite timeout to use extend()')
    raise

Prevention

When it happens

Trigger: Constructing Lock(redis, name, timeout=None) (or omitting timeout when the client default is None) and then calling extend(additional_time); acquiring a permanent lock and attempting to extend its TTL.

Common situations: Passing timeout=0 or timeout=None intending a long-lived lock but then needing to refresh it; copy-pasting extend/reacquire logic from a TTL-based lock into a permanent-lock workflow; assuming the client's default socket_timeout also sets the lock timeout.

Understand the failure class

Related errors


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