redis/redis-py · warning · LockError
Cannot release a lock that's not owned or is already unlocke
Error message
Cannot release a lock that's not owned or is already unlocked.
What it means
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.
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 da03cdc7e8)
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).
Example fix
// before lock = Lock(redis, 'k', thread_local=True) await lock.acquire() # in task A await loop.run_in_executor(None, lambda: asyncio.run(lock.release())) # task B -> error // after lock = Lock(redis, 'k', thread_local=False) await lock.acquire() # in task A await lock.release() # works from task B
Defensive patterns
Strategy: validation
Validate before calling
async def release_if_owned(lock):
if lock.local.token is not None:
await lock.release()
# else: nothing to release Type guard
def lock_holds_token(lock) -> bool:
return getattr(getattr(lock, 'local', None), 'token', None) is not None Try / catch
from redis.exceptions import LockError
try:
await lock.release()
except LockError as e:
if 'not owned' in str(e):
# already released / never acquired; safe to ignore
... Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot release a lock that's no longer owned
- Cannot extend an unlocked lock
- Cannot extend a lock that's no longer owned
- Cannot reacquire an unlocked lock
- Unable to acquire lock within the time specified
AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04).
Data as JSON: /data/errors/7e9305955ba91a10.json.
Report an issue: GitHub.