{"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":"warning","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/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/lock.py#L276-L312","documentation":"Raised as LockNotOwnedError (subclass of LockError) by Lock.do_release() when the Lua release script returns 0: the key in Redis is missing or its stored token does not match this lock's expected_token. The lock is not (or no longer) owned by this lock instance, so the delete is skipped to avoid releasing someone else's lock.","triggerScenarios":"The lock TTL expired before release() was called (owner crashed or was slow); another owner already acquired after expiry; the key was deleted out-of-band; a token mismatch due to thread_local confusion. Fires at lock.py:293-294.","commonSituations":"Critical section took longer than the lock timeout; the owning process restarted; someone manually DEL'd the key; double-release across tasks after the TTL lapsed.","solutions":["Make the lock timeout longer than your worst-case critical section, or extend() the lock before it expires.","Keep critical sections short and bounded.","If you expect this (e.g., best-effort locking), catch LockNotOwnedError and treat as already-released.","Investigate out-of-band key deletion if it recurs unexpectedly."],"exampleFix":"// before\nlock = Lock(redis, 'k', timeout=2)\nawait lock.acquire()\nawait long_task_taking_5s()  # TTL expires\nawait lock.release()  # LockNotOwnedError\n// after\nlock = Lock(redis, 'k', timeout=30)\nawait lock.acquire()\nawait long_task_taking_5s()\nawait lock.release()","handlingStrategy":"try-catch","validationCode":"async def release_if_still_owned(lock):\n    if await lock.owned():\n        await lock.release()\n    # else: TTL expired / lost; nothing to release","typeGuard":"async def lock_still_owned(lock) -> bool:\n    return await lock.owned()","tryCatchPattern":"from redis.exceptions import LockNotOwnedError\ntry:\n    await lock.release()\nexcept LockNotOwnedError:\n    # lock TTL expired or lost; treat as released\n    ...","preventionTips":["Set timeout > worst-case critical section duration.","extend() the lock before its TTL expires.","Catch LockNotOwnedError for best-effort locking."],"tags":["lock","ttl","ownership","asyncio"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}