{"record":{"id":"b3e9e023b419f988","repo":"redis/redis-py","slug":"cannot-release-a-lock-that-s-not-owned-or-is-alrea-b3e9e0","errorCode":null,"errorMessage":"Cannot release a lock that's not owned or is already unlocked.","messagePattern":"Cannot release a lock that's not owned or is already unlocked\\.","errorType":"exception","errorClass":"LockError","httpStatus":null,"severity":"error","filePath":"redis/lock.py","lineNumber":271,"sourceCode":"    def owned(self) -> bool:\n        \"\"\"\n        Returns True if this key is locked by this lock, otherwise False.\n        \"\"\"\n        stored_token = self.redis.get(self.name)\n        # need to always compare bytes to bytes\n        # TODO: this can be simplified when the context manager is finished\n        if stored_token and not isinstance(stored_token, bytes):\n            encoder = self.redis.get_encoder()\n            stored_token = encoder.encode(stored_token)\n        return self.local.token is not None and stored_token == self.local.token\n\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]:","sourceCodeStart":253,"sourceCodeEnd":289,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/lock.py#L253-L289","documentation":"Lock.release (redis/lock.py:271) raises LockError('Cannot release a lock that's not owned or is already unlocked.') when self.local.token is None — meaning release() was called before any successful acquire() on this thread/context, or release() was called twice (the first release clears the token). This is a client-side ownership check before the server-side Lua release runs.","triggerScenarios":"Calling lock.release() without having acquired the lock; calling release() twice (the second call sees token already None); using thread_local=True and calling release from a different thread than the one that acquired (the token is in thread-local storage); exiting the context manager twice.","commonSituations":"Releasing in a finally block when acquire failed; double-release in nested cleanup; cross-thread release with the default thread_local=True; a bug where release runs on an un-acquired lock instance.","solutions":["Only call release() after a successful acquire() (or inside the `with` block after successful entry).","Guard against double-release by tracking ownership yourself, or rely on the context manager which releases once.","If releasing across threads, construct the Lock with thread_local=False so the token is shared."],"exampleFix":"// before\nlock.release()  # in finally, even if acquire failed\n// after\nacquired = lock.acquire(blocking=False)\nif acquired:\n    try:\n        do_work()\n    finally:\n        lock.release()","handlingStrategy":"try-catch","validationCode":"def release_if_owned(lock):\n    if getattr(lock.local, 'token', None) is not None:\n        lock.release()","typeGuard":"def lock_is_locally_owned(lock) -> bool:\n    return getattr(lock.local, 'token', None) is not None","tryCatchPattern":"from redis.exceptions import LockError\ntry:\n    lock.release()\nexcept LockError:\n    pass  # nothing to release","preventionTips":["Only release after a successful acquire; guard release in finally with an ownership flag.","Avoid double-release; rely on the context manager for single release.","Use thread_local=False if you must release from a different thread than the acquirer."],"tags":["lock","distributed-lock","lifecycle","double-release"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}