{"id":"7e9305955ba91a10","repo":"redis/redis-py","slug":"cannot-release-a-lock-that-s-not-owned-or-is-alrea","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":"warning","filePath":"redis/asyncio/lock.py","lineNumber":275,"sourceCode":"        if stored_token and not isinstance(stored_token, bytes):\n            try:\n                encoder = self.redis.connection_pool.get_encoder()\n            except AttributeError:\n                # Cluster\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    async def release(self) -> None:\n        \"\"\"Releases the already acquired lock.\n\n        The token is only cleared after the Redis release operation completes\n        successfully. This ensures that if the release is cancelled mid-operation,\n        the lock state remains consistent and can be retried.\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        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        ):","sourceCodeStart":257,"sourceCodeEnd":293,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/lock.py#L257-L293","documentation":"Raised as LockError by Lock.release() when self.local.token is None, meaning this lock instance has no recorded ownership token. Either it was never acquired, was already released, or (with thread_local=True) the releasing thread differs from the acquiring thread. The library refuses to send a release script with no token.","triggerScenarios":"Calling lock.release() before lock.acquire() succeeded; calling release() twice; releasing from a different task/thread than the one that acquired when thread_local=True (default). Fires at lock.py:274-275.","commonSituations":"Double-release in a finally block; releasing in a different asyncio task that did not acquire (thread-local token not visible); a cancelled acquire that still entered a finally; mismatched acquire/release pairing across tasks.","solutions":["Ensure release() is called exactly once after a successful acquire(), ideally in a finally tied to the same acquire.","If releasing from a different task, construct the Lock with thread_local=False so the token is shared.","Track ownership yourself and skip release when not owned (or use owned() to check)."],"exampleFix":"// before\nlock = Lock(redis, 'k', thread_local=True)\nawait lock.acquire()  # in task A\nawait loop.run_in_executor(None, lambda: asyncio.run(lock.release()))  # task B -> error\n// after\nlock = Lock(redis, 'k', thread_local=False)\nawait lock.acquire()  # in task A\nawait lock.release()  # works from task B","handlingStrategy":"validation","validationCode":"async def release_if_owned(lock):\n    if lock.local.token is not None:\n        await lock.release()\n    # else: nothing to release","typeGuard":"def lock_holds_token(lock) -> bool:\n    return getattr(getattr(lock, 'local', None), 'token', None) is not None","tryCatchPattern":"from redis.exceptions import LockError\ntry:\n    await lock.release()\nexcept LockError as e:\n    if 'not owned' in str(e):\n        # already released / never acquired; safe to ignore\n        ...","preventionTips":["Release exactly once after a successful acquire, in a finally.","Use thread_local=False when releasing from a different task.","Track ownership to avoid double-release."],"tags":["lock","ownership","thread-local","asyncio"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}