redis/redis-py · error · LockNotOwnedError

Cannot reacquire a lock that's no longer owned

Error message

Cannot reacquire a lock that's no longer owned

What it means

Raised by Lock.do_reacquire() (the async distributed lock) when the server-side Lua reacquire script returns 0. That return value means the lock key no longer exists at the given name, or its value no longer matches this Lock's token, so this client no longer owns it. Reacquire resets an existing lock's TTL back to its configured timeout, so it is only valid while ownership is still held server-side. The lock is most commonly lost because the TTL elapsed before reacquire was called.

Source

Thrown at redis/asyncio/lock.py:344

    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
            )
        ):
            raise LockNotOwnedError("Cannot reacquire a lock that's no longer owned")
        return True

View on GitHub (pinned to da03cdc7e8)

Solutions

  1. Catch `LockNotOwnedError` from `reacquire()` and treat it as 'lost lock' — re-acquire from scratch with `await lock.acquire()` before continuing critical work.
  2. Increase the lock `timeout` (or call `extend` more frequently) so reacquire happens before the server TTL expires.
  3. Audit for code paths that release the lock concurrently while another task calls reacquire on the same Lock instance.
  4. Confirm no external process (admin tool, another service, Redis eviction) is deleting the lock key.

Example fix

# before
lock = client.lock('res', timeout=5)
await lock.acquire()
# ... long work ...
await lock.reacquire()  # raises LockNotOwnedError after 5s

# after
from redis.exceptions import LockNotOwnedError
try:
    await lock.reacquire()
except LockNotOwnedError:
    await lock.acquire()  # re-acquire ownership before continuing
Defensive patterns

Strategy: try-catch

Validate before calling

import redis.asyncio as redis

async def owns_lock(client, lock) -> bool:
    # Cheap pre-check: key exists and token matches before reacquire.
    tok = await client.get(lock.name)
    return tok is not None and tok == lock.local.token

Type guard

from redis.asyncio import Lock

def is_held_lock(obj) -> bool:
    return isinstance(obj, Lock) and obj.local.token is not None and obj.timeout is not None

Try / catch

from redis.exceptions import LockNotOwnedError

try:
    await lock.reacquire()
except LockNotOwnedError:
    # ownership lost (TTL elapsed / key gone) -> re-acquire
    await lock.acquire()

Prevention

When it happens

Trigger: Calling `await lock.reacquire()` after the lock's TTL has expired on the Redis server, after another client overwrote/deleted the key, or after the same Lock object already released it. Also triggered if `lock.local.token` is set (so the `Cannot reacquire an unlocked lock` guard at lock.py:332 is passed) but the server key is gone. The precondition check at lock.py:333 also requires `self.timeout is not None`.

Common situations: Setting a short `timeout` on the lock and calling reacquire too late; long-running work that outlasts the TTL; a Redis FLUSHDB/eviction removing the key; clock drift between client and server making the client believe the lock is still valid; sharing a Lock object across coroutines and one branch releases it while another reacquires.

Related errors


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