redis/redis-py · error · WatchError
A occurred while watching one or more keys
Error message
A {type(error).__name__} occurred while watching one or more keys What it means
Raised by Pipeline._disconnect_reset_raise_on_watching during the immediate-command path (WATCH + pre-MULTI reads) when the connection fails with a retryable error and self.watching is True. Because a watched key's validity is tied to the connection, any disconnect invalidates the WATCH; the library resets state and raises WatchError so the caller retries the whole transaction.
Solutions
- Wrap the WATCH->read->MULTI->EXEC sequence in a retry loop that catches WatchError and re-runs the entire transaction.
- Raise conn.retry retries (Retry(backoff=..., retries=N)) to absorb transient errors before they surface as WatchError.
- Increase socket_timeout if the read between WATCH and MULTI is slow.
Example fix
// before
async with client.pipeline(transaction=True) as pipe:
await pipe.watch('k')
v = await pipe.get('k')
pipe.multi()
await pipe.set('k', process(v))
await pipe.execute()
// after
for _ in range(max_attempts):
try:
async with client.pipeline(transaction=True) as pipe:
await pipe.watch('k')
v = await pipe.get('k')
pipe.multi()
await pipe.set('k', process(v))
await pipe.execute()
break
except WatchError:
continue Defensive patterns
Strategy: retry
Validate before calling
# No pure-client precondition for a network failure mid-WATCH. # The defense is a retry loop around the whole transaction.
Try / catch
from redis.exceptions import WatchError
for _ in range(max_attempts):
try:
async with client.pipeline(transaction=True) as pipe:
await pipe.watch('k')
v = await pipe.get('k')
pipe.multi()
await pipe.set('k', process(v))
await pipe.execute()
break
except WatchError:
continue Prevention
- Always wrap WATCH-based transactions in a bounded retry loop.
- Tune Retry(retries=...) and socket_timeout to absorb transient network errors.
When it happens
Trigger: Issuing WATCH then a read (e.g. await pipe.get(key)) inside a multi-step transaction, and the socket raises ConnectionError / TimeoutError that the retry layer could not recover within conn.retry.get_retries(). The f-string names the underlying error class (e.g. ConnectionError).
Common situations: Network blips or a Redis restart mid-transaction; aggressive socket_timeout on long WATCH+read sequences; containerized deployments where the load balancer closes idle connections between WATCH and EXEC.
Related errors
- A occurred while watching one or more keys
- Slot rebalancing occurred while watching keys
- Cannot identify slot number for command
- Cannot issue a WATCH after a MULTI
- Cannot issue a WATCH after a MULTI
AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10).
Data as JSON: /api/errors/602f64df4fb77554.
Report an issue: GitHub.
Appendix: source
Thrown at redis/asyncio/client.py:1947
and failure_count is not None
and failure_count <= conn.retry.get_retries()
):
await record_operation_duration(
command_name=command_name,
duration_seconds=time.monotonic() - start_time,
server_address=getattr(conn, "host", None),
server_port=getattr(conn, "port", None),
db_namespace=str(conn.db),
error=error,
retry_attempts=failure_count,
)
await conn.disconnect(error=error, failure_count=failure_count)
# if we were already watching a variable, the watch is no longer
# valid since this connection has died. raise a WatchError, which
# indicates the user should retry this transaction.
if self.watching:
await self.reset()
raise WatchError(
f"A {type(error).__name__} occurred while watching one or more keys"
)
async def immediate_execute_command(self, *args, **options):
"""
Execute a command immediately, but don't auto-retry on the supported
errors for retry if we're already WATCHing a variable.
Used when issuing WATCH or subsequent commands retrieving their values but before
MULTI is called.
"""
command_name = args[0]
conn = self.connection
# if this is the first call, we need a connection
if not conn:
conn = await self.connection_pool.get_connection()
self.connection = conn
# Start timing for observabilityView on GitHub (pinned to 6a6b581b48)