{"id":"20259ed6a803e484","repo":"redis/redis-py","slug":"cannot-extend-a-lock-with-no-timeout","errorCode":null,"errorMessage":"Cannot extend a lock with no timeout","messagePattern":"Cannot extend a lock with no timeout","errorType":"exception","errorClass":"LockError","httpStatus":null,"severity":"warning","filePath":"redis/asyncio/lock.py","lineNumber":312,"sourceCode":"            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\")\n        return self.do_extend(additional_time, replace_ttl)\n\n    async def do_extend(self, additional_time, replace_ttl) -> Literal[True]:\n        additional_time = int(additional_time * 1000)\n        if not bool(\n            await self.lua_extend(\n                keys=[self.name],\n                args=[self.local.token, additional_time, replace_ttl and \"1\" or \"0\"],\n                client=self.redis,\n            )\n        ):\n            raise LockNotOwnedError(\"Cannot extend a lock that's no longer owned\")\n        return True\n\n    def reacquire(self) -> Awaitable[Literal[True]]:\n        \"\"\"\n        Resets a TTL of an already acquired lock back to a timeout value.\n        \"\"\"","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/lock.py#L294-L330","documentation":"Raised as LockError by Lock.extend() when self.timeout is None, i.e., the Lock was constructed without a timeout. extend() needs the original timeout semantics to compute the new TTL; a lock with no timeout (manual-release-only) has no TTL to extend.","triggerScenarios":"Constructing Lock(redis, name, timeout=None) (the default) and then calling lock.extend(...). The guard at lock.py:311-312 fires before any Redis call.","commonSituations":"Using a perpetual lock and later deciding to add a TTL via extend; copying config from a manual-release lock into an extend-based keepalive flow.","solutions":["Construct the Lock with a finite timeout (e.g., timeout=30) so extend() has a TTL to work with.","If you want to add a TTL after the fact, reacquire the lock with a timeout instead of extend().","Use EXPIRE directly via redis.expire(lock.name, ttl) only if you understand the ownership implications."],"exampleFix":"// before\nlock = Lock(redis, 'k')  # timeout=None default\nawait lock.acquire()\nlock.extend(30)  # LockError\n// after\nlock = Lock(redis, 'k', timeout=30)\nawait lock.acquire()\nlock.extend(30)","handlingStrategy":"validation","validationCode":"def can_extend(lock) -> bool:\n    return lock.timeout is not None\n\nif not can_extend(lock):\n    lock = Lock(redis, name, timeout=30)  # recreate with a TTL\n\nawait lock.acquire()\nlock.extend(30)","typeGuard":"def lock_has_timeout(lock) -> bool:\n    return lock.timeout is not None","tryCatchPattern":"from redis.exceptions import LockError\ntry:\n    lock.extend(10)\nexcept LockError as e:\n    if 'no timeout' in str(e):\n        # recreate the lock with a timeout\n        ...","preventionTips":["Construct Lock with a finite timeout if you intend to extend.","Don't use extend() on manual-release (timeout=None) locks.","For adding a TTL after the fact, reacquire with a timeout."],"tags":["lock","timeout","extend","asyncio"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}