redis/redis-py · error · LockError

Cannot release a lock that's not owned or is already…

Error message

Cannot release a lock that's not owned or is already unlocked.

What it means

release() reads the local token; if it is None the lock was never acquired on this instance (or was already released in this task), so the client refuses to run the Lua release and raises LockError. With thread_local=True (default) the token is stored per-thread, so releasing from a different thread/task than the one that acquired looks like 'not owned'.

Solutions

  1. Only release after a successful acquire; guard with 'if await lock.owned()'.
  2. For cross-thread/task handoff, construct the Lock with thread_local=False.
  3. In context-manager usage set raise_on_release_error=False to suppress release errors on exit.
  4. Avoid calling release() twice.

Example fix

# before
lock = Lock(r, 'resource')  # thread_local=True default
# acquired in task A, released in task B -> LockError
# after
lock = Lock(r, 'resource', thread_local=False)
# token visible across tasks
Defensive patterns

Strategy: try-catch

Validate before calling

if await lock.owned():
    await lock.release()

Try / catch

from redis.exceptions import LockError
try:
    await lock.release()
except LockError:
    # never owned on this task: safe to ignore or log
    ...

Prevention

When it happens

Trigger: Calling release() before acquire(); calling release() twice; releasing from a different thread/task than acquired (thread_local=True); context-manager exit after a failed/contended acquisition.

Common situations: Cross-thread or cross-task lock handoff without thread_local=False; double release in finally blocks; reusing a Lock object and releasing before acquiring.

Related errors


AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10). Data as JSON: /api/errors/7e9305955ba91a10. Report an issue: GitHub.

Appendix: source

Thrown at redis/asyncio/lock.py:275

        if stored_token and not isinstance(stored_token, bytes):
            try:
                encoder = self.redis.connection_pool.get_encoder()
            except AttributeError:
                # Cluster
                encoder = self.redis.get_encoder()
            stored_token = encoder.encode(stored_token)
        return self.local.token is not None and stored_token == self.local.token

    async def release(self) -> None:
        """Releases the already acquired lock.

        The token is only cleared after the Redis release operation completes
        successfully. This ensures that if the release is cancelled mid-operation,
        the lock state remains consistent and can be retried.
        """
        expected_token = self.local.token
        if expected_token is None:
            raise LockError(
                "Cannot release a lock that's not owned or is already unlocked.",
                lock_name=self.name,
            )
        try:
            await self.do_release(expected_token)
        except LockNotOwnedError:
            # Lock doesn't exist in Redis, safe to clear token
            self.local.token = None
            raise
        # Only clear token after successful release
        self.local.token = None

    async def do_release(self, expected_token: bytes) -> None:
        if not bool(
            await self.lua_release(
                keys=[self.name], args=[expected_token], client=self.redis
            )
        ):

View on GitHub (pinned to 6a6b581b48)