{"record":{"id":"8fa21cfed0055188","repo":"redis/redis-py","slug":"cannot-release-a-lock-that-s-no-longer-owned-8fa21c","errorCode":null,"errorMessage":"Cannot release a lock that's no longer owned","messagePattern":"Cannot release a lock that's no longer owned","errorType":"exception","errorClass":"LockNotOwnedError","httpStatus":null,"severity":"error","filePath":"redis/lock.py","lineNumber":282,"sourceCode":"\n    def release(self) -> None:\n        \"\"\"\n        Releases the already acquired lock\n        \"\"\"\n        expected_token = self.local.token\n        if expected_token is None:\n            raise LockError(\n                \"Cannot release a lock that's not owned or is already unlocked.\",\n                lock_name=self.name,\n            )\n        self.local.token = None\n        self.do_release(expected_token)\n\n    def do_release(self, expected_token: str) -> None:\n        if not bool(\n            self.lua_release(keys=[self.name], args=[expected_token], client=self.redis)\n        ):\n            raise LockNotOwnedError(\n                \"Cannot release a lock that's no longer owned\",\n                lock_name=self.name,\n            )\n\n    def extend(\n        self, additional_time: Number, replace_ttl: bool = False\n    ) -> Literal[True]:\n        \"\"\"\n        Adds more time to an already acquired lock.\n\n        ``additional_time`` can be specified as an integer or a float, both\n        representing the number of seconds to add.\n\n        ``replace_ttl`` if False (the default), add `additional_time` to\n        the lock's existing ttl. If True, replace the lock's ttl with\n        `additional_time`.\n        \"\"\"\n        if self.local.token is None:","sourceCodeStart":264,"sourceCodeEnd":300,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/lock.py#L264-L300","documentation":"Lock.do_release (redis/lock.py:282) raises LockNotOwnedError (a LockError subclass) when the server-side Lua release script returns falsy — the token stored at the lock key did not match the expected token. That means the lock was never held, already expired (timeout elapsed), or was stolen/re-acquired by another holder between acquire and release. Unlike error 438 (a client-side pre-check), this is the server confirming you no longer own the key.","triggerScenarios":"release() after the lock TTL expired (your critical section took longer than the lock timeout); releasing a lock whose key was overwritten; the token lost because of a failover where the key lived on a node that went down; releasing after manually deleting the key.","commonSituations":"Long-running work exceeding the lock timeout so the lock silently expired and another worker took it; no extend() calls for long sections; a failover losing the lock key; mixing manual DEL with the Lock abstraction.","solutions":["Set a lock timeout comfortably larger than your worst-case critical section, or call lock.extend() periodically for long work.","Catch LockNotOwnedError on release and treat it as 'I may not have been the sole owner' — re-check/recover the protected resource.","Use raise_on_release_error=True (default False in __exit__) only if losing ownership must be fatal; otherwise log it.","Avoid manual del on the lock key; always release through the Lock API."],"exampleFix":"// before\nwith Lock(r, 'task', timeout=5):\n    long_running_task()  # may exceed 5s, lock expires\n// after\nfrom redis.exceptions import LockNotOwnedError\nwith Lock(r, 'task', timeout=60) as lock:\n    while working:\n        do_chunk(); lock.extend(60)\n# and on release:\ntry:\n    lock.release()\nexcept LockNotOwnedError:\n    log.warning('lock expired before release; verify protected state')","handlingStrategy":"try-catch","validationCode":"def still_owned(lock) -> bool:\n    return lock.owned()","typeGuard":"def lock_is_owned(lock) -> bool:\n    return lock.owned()","tryCatchPattern":"from redis.exceptions import LockNotOwnedError\ntry:\n    lock.release()\nexcept LockNotOwnedError:\n    log.warning('lock expired before release; re-verify protected resource')","preventionTips":["Set the lock timeout above worst-case work duration, or call lock.extend() periodically for long sections.","Treat a release-time LockNotOwnedError as a signal to re-check the protected resource.","Never manually DEL the lock key; always release through the Lock API.","Keep raise_on_release_error=False (default) unless losing ownership must be fatal."],"tags":["lock","distributed-lock","ownership","ttl-expiry"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}