{"record":{"id":"cb27b714ad81b7c8","repo":"redis/redis-py","slug":"cannot-reacquire-a-lock-that-s-no-longer-owned-cb27b7","errorCode":null,"errorMessage":"Cannot reacquire a lock that's no longer owned","messagePattern":"Cannot reacquire a lock that's no longer owned","errorType":"exception","errorClass":"LockNotOwnedError","httpStatus":null,"severity":"error","filePath":"redis/lock.py","lineNumber":341,"sourceCode":"        Resets a TTL of an already acquired lock back to a timeout value.\n        \"\"\"\n        if self.local.token is None:\n            raise LockError(\"Cannot reacquire an unlocked lock\", lock_name=self.name)\n        if self.timeout is None:\n            raise LockError(\n                \"Cannot reacquire a lock with no timeout\",\n                lock_name=self.name,\n            )\n        return self.do_reacquire()\n\n    def do_reacquire(self) -> Literal[True]:\n        timeout = int(self.timeout * 1000)\n        if not bool(\n            self.lua_reacquire(\n                keys=[self.name], args=[self.local.token, timeout], client=self.redis\n            )\n        ):\n            raise LockNotOwnedError(\n                \"Cannot reacquire a lock that's no longer owned\",\n                lock_name=self.name,\n            )\n        return True\n","sourceCodeStart":323,"sourceCodeEnd":346,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/lock.py#L323-L346","documentation":"Raised as LockNotOwnedError by Lock.do_reacquire() (lock.py:341) when the server-side LUA_REACQUIRE 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 by another owner. This fires after a Redis round-trip and means ownership was lost server-side.","triggerScenarios":"Calling reacquire() after the lock's TTL already elapsed; another client acquired the same lock name after expiry; the key was evicted/flushed; server clock skew causing premature expiry.","commonSituations":"Heartbeat loops that wake up after the TTL window has closed; contention where a second worker grabbed the lock once it expired; Redis memory eviction; long GC pauses delaying the reacquire past TTL.","solutions":["Catch LockNotOwnedError and re-acquire the lock before continuing protected work.","Make the heartbeat/reacquire interval shorter than the lock timeout (e.g. reacquire every timeout/3).","Check lock.owned() before reacquire() and re-acquire if False.","Tune the timeout upward relative to worst-case heartbeat latency."],"exampleFix":"# before\nwhile working:\n    lock.reacquire()\n    do_chunk()\n\n# after\nwhile working:\n    try:\n        lock.reacquire()\n    except LockNotOwnedError:\n        if not lock.acquire(blocking=True, blocking_timeout=10):\n            break  # someone else owns it; stop\n    do_chunk()","handlingStrategy":"try-catch","validationCode":"# Pre-check ownership server-side before reacquiring\nif not lock.owned():\n    raise RuntimeError('lock not owned server-side; re-acquire')\nlock.reacquire()","typeGuard":"from redis.lock import Lock\n\ndef still_owned(lock: Lock) -> bool:\n    return lock.owned()","tryCatchPattern":"from redis.exceptions import LockNotOwnedError\n\ntry:\n    lock.reacquire()\nexcept LockNotOwnedError:\n    if not lock.acquire(blocking=True, blocking_timeout=10):\n        raise RuntimeError('could not re-acquire expired lock')","preventionTips":["Run heartbeat/reacquire at an interval shorter than the timeout (e.g. timeout/3).","Account for GC pauses and clock skew when sizing the interval.","Always handle LockNotOwnedError in heartbeat loops."],"tags":["lock","distributed-lock","ownership-lost","ttl-expiry"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}