{"record":{"id":"2664c1c275c9c338","repo":"redis/redis-py","slug":"cannot-release-a-lock-that-s-no-longer-owned","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/asyncio/lock.py","lineNumber":294,"sourceCode":"                \"Cannot release a lock that's not owned or is already unlocked.\",\n                lock_name=self.name,\n            )\n        try:\n            await self.do_release(expected_token)\n        except LockNotOwnedError:\n            # Lock doesn't exist in Redis, safe to clear token\n            self.local.token = None\n            raise\n        # Only clear token after successful release\n        self.local.token = None\n\n    async def do_release(self, expected_token: bytes) -> None:\n        if not bool(\n            await self.lua_release(\n                keys=[self.name], args=[expected_token], client=self.redis\n            )\n        ):\n            raise LockNotOwnedError(\"Cannot release a lock that's no longer owned\")\n\n    def extend(\n        self, additional_time: Number, replace_ttl: bool = False\n    ) -> Awaitable[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:\n            raise LockError(\"Cannot extend an unlocked lock\")\n        if self.timeout is None:\n            raise LockError(\"Cannot extend a lock with no timeout\")","sourceCodeStart":276,"sourceCodeEnd":312,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/asyncio/lock.py#L276-L312","documentation":"do_release runs the Lua release script that checks the stored token; if the key is gone (TTL expired) or the token differs, it returns 0 and LockNotOwnedError is raised. This is server-side confirmation the lock is no longer yours, most commonly because its TTL expired while you were still in the critical section.","triggerScenarios":"Calling release() after the lock's timeout/TTL expired; the key was deleted or overwritten by another client; token mismatch.","commonSituations":"Critical section longer than timeout; manual del of the lock key; another holder overwrote it after expiry.","solutions":["Ensure critical sections finish well within timeout, or extend() the lock proactively before it expires.","Treat LockNotOwnedError as 'lock expired, the work may be unsafe' and abort/log.","Use a longer timeout or a renewal task.","Never manually delete lock keys."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"from redis.exceptions import LockNotOwnedError\ntry:\n    await lock.release()\nexcept LockNotOwnedError:\n    # TTL expired mid-section: work may be unsafe, abort/log\n    ...","preventionTips":["Finish critical sections well within timeout, or extend() proactively.","Treat LockNotOwnedError as an expired-lock signal and make the operation abort-safe.","Never manually delete lock keys."],"tags":["lock","ttl","distributed-lock","ownership","async"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}