{"record":{"id":"2ad574537f19c1ce","repo":"redis/redis-py","slug":"cannot-extend-a-lock-that-s-no-longer-owned-2ad574","errorCode":null,"errorMessage":"Cannot extend a lock that's no longer owned","messagePattern":"Cannot extend a lock that's no longer owned","errorType":"exception","errorClass":"LockNotOwnedError","httpStatus":null,"severity":"error","filePath":"redis/lock.py","lineNumber":315,"sourceCode":"        the lock's existing ttl. If True, replace the lock's ttl with\n        `additional_time`.\n        \"\"\"\n        if self.local.token is None:\n            raise LockError(\"Cannot extend an unlocked lock\", lock_name=self.name)\n        if self.timeout is None:\n            raise LockError(\"Cannot extend a lock with no timeout\", lock_name=self.name)\n        return self.do_extend(additional_time, replace_ttl)\n\n    def do_extend(self, additional_time: Number, replace_ttl: bool) -> Literal[True]:\n        additional_time = int(additional_time * 1000)\n        if not bool(\n            self.lua_extend(\n                keys=[self.name],\n                args=[self.local.token, additional_time, \"1\" if replace_ttl else \"0\"],\n                client=self.redis,\n            )\n        ):\n            raise LockNotOwnedError(\n                \"Cannot extend a lock that's no longer owned\",\n                lock_name=self.name,\n            )\n        return True\n\n    def reacquire(self) -> Literal[True]:\n        \"\"\"\n        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","sourceCodeStart":297,"sourceCodeEnd":333,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/lock.py#L297-L333","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Catch LockNotOwnedError specifically and re-acquire the lock before continuing critical work.","Increase the base timeout or call extend() more frequently (well before the TTL expires).","Check lock.owned() immediately before extend() and skip/handle gracefully if False.","Review the work segment length so it completes well within the timeout window."],"exampleFix":"# before\nlock.extend(5)\n\n# after\ntry:\n    lock.extend(5)\nexcept LockNotOwnedError:\n    # lock expired — must re-acquire before doing more protected work\n    if lock.acquire(blocking=True, blocking_timeout=10):\n        do_critical_work()\n    else:\n        abort_work()","handlingStrategy":"try-catch","validationCode":"# Pre-check ownership server-side before extending\nif not lock.owned():\n    # ownership already lost — re-acquire instead of extending\n    raise RuntimeError('lock not owned server-side; re-acquire')\nlock.extend(additional_time)","typeGuard":"from redis.lock import Lock\n\ndef still_owned(lock: Lock) -> bool:\n    \"\"\"True when the server still holds this client's token.\"\"\"\n    return lock.owned()","tryCatchPattern":"from redis.exceptions import LockNotOwnedError\n\ntry:\n    lock.extend(additional_time)\nexcept LockNotOwnedError:\n    # TTL expired / taken over — must re-acquire before continuing protected work\n    if not lock.acquire(blocking=True, blocking_timeout=10):\n        raise RuntimeError('could not re-acquire expired lock')","preventionTips":["Call extend() well before the TTL expires (e.g. every timeout/3).","Make processing segments shorter than the lock timeout.","Always handle LockNotOwnedError on extend() in long-running jobs."],"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"}