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

  1. Wrap the WATCH->read->MULTI->EXEC sequence in a retry loop that catches WatchError and re-runs the entire transaction.
  2. Raise conn.retry retries (Retry(backoff=..., retries=N)) to absorb transient errors before they surface as WatchError.
  3. 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

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


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 observability

View on GitHub (pinned to 6a6b581b48)