redis/redis-py · error · LockError

Cannot reacquire an unlocked lock

Error message

Cannot reacquire an unlocked lock

What it means

Raised by Lock.reacquire() when self.local.token is None, meaning the current thread holds no ownership token. reacquire() resets the lock's TTL back to its original timeout value, which requires the lock to currently be held by this thread. This is a client-side precondition check (LockError) at lock.py:326 fired before any Redis call.

Source

Thrown at redis/lock.py:326

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

    def do_reacquire(self) -> Literal[True]:
        timeout = int(self.timeout * 1000)
        if not bool(
            self.lua_reacquire(
                keys=[self.name], args=[self.local.token, timeout], client=self.redis
            )
        ):
            raise LockNotOwnedError(
                "Cannot reacquire a lock that's no longer owned",
                lock_name=self.name,
            )

View on GitHub (pinned to 6a6b581b48)

Solutions

  1. Ensure acquire() returned True before calling reacquire().
  2. Use the context manager (with lock:) to keep acquire/release balanced.
  3. Guard with lock.owned() before reacquire().
  4. Set thread_local=False when the lock must be reacquired by a different thread than the acquirer.

Example fix

# before
lock = client.lock('mylock', timeout=30)
lock.reacquire()  # raises — not acquired

# after
lock = client.lock('mylock', timeout=30)
lock.acquire()
try:
    lock.reacquire()
finally:
    lock.release()
Defensive patterns

Strategy: validation

Validate before calling

# Validate ownership before reacquiring
if lock.local.token is None or not lock.owned():
    raise RuntimeError('cannot reacquire: lock not held by this thread')
lock.reacquire()

Type guard

from redis.lock import Lock

def is_held(lock: Lock) -> bool:
    return lock.local.token is not None and lock.owned()

Try / catch

from redis.exceptions import LockError

try:
    lock.reacquire()
except LockError as e:
    if 'unlocked' in str(e):
        if lock.acquire(blocking=True, blocking_timeout=5):
            lock.reacquire()
    else:
        raise

Prevention

When it happens

Trigger: Calling lock.reacquire() before lock.acquire() succeeded; calling reacquire() after release(); calling reacquire() from a worker thread that did not perform the acquire when thread_local=True (default).

Common situations: Resetting a lock TTL in a background loop that started before acquire completed; reacquiring in cleanup paths that run after release; sharing a Lock object across threads without thread_local=False.

Related errors


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