redis/redis-py · error · WatchError

Slot rebalancing occurred while watching keys

Error message

Slot rebalancing occurred while watching keys

What it means

WatchError raised in TransactionStrategy._reinitialize_on_error (redis/asyncio/cluster.py:3303) when an ASK or MOVED redirect arrives while WATCH is active and the transaction is mid-execution. A slot move invalidates the watched-key connection pinning (WATCH state is connection-local), so the transaction cannot transparently retry. The caller must redo the whole watch/transaction sequence.

Solutions

  1. Wrap the watch/multi/exec sequence in a retry loop that re-runs WATCH + transaction on WatchError
  2. Pause or avoid cluster reshard during critical transactions
  3. Use optimistic concurrency at the application level (version checks) instead of WATCH under unstable topology

Example fix

// before
await pipe.watch('k1')
...multi/exec...
// after
for _ in range(retries):
    try:
        await pipe.watch('k1')
        ...multi/exec...
        break
    except WatchError:
        await pipe.reset()
Defensive patterns

Strategy: retry

Validate before calling

# cannot fully prevent; topology is external. Detect and retry.
from redis.exceptions import WatchError

Try / catch

for _ in range(MAX_RETRIES):
    try:
        await pipe.watch('k1'); pipe.multi(); ...; await pipe.execute(); break
    except WatchError:
        await pipe.reset()

Prevention

When it happens

Trigger: A cluster reshard, failover, or slot migration is in progress while a watched transaction runs, producing an ASK/MOVED error during EXEC.

Common situations: Operating against a cluster undergoing scaling operations; long-running transactions that outlast a rebalance window.

Related errors


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

Appendix: source

Thrown at redis/asyncio/cluster.py:3303

        if command_name in self.UNWATCH_COMMANDS:
            self._watching = False
        return output

    async def _reinitialize_on_error(self, error, failure_count):
        if hasattr(error, "connection"):
            await record_error_count(
                server_address=error.connection.host,
                server_port=error.connection.port,
                network_peer_address=error.connection.host,
                network_peer_port=error.connection.port,
                error_type=error,
                retry_attempts=failure_count,
                is_internal=True,
            )

        if self._watching:
            if type(error) in self.SLOT_REDIRECT_ERRORS and self._executing:
                raise WatchError("Slot rebalancing occurred while watching keys")

        if (
            type(error) in self.SLOT_REDIRECT_ERRORS
            or type(error) in self.CONNECTION_ERRORS
        ):
            if self._transaction_connection and self._transaction_node:
                # Disconnect and release back to pool
                await self._transaction_connection.disconnect()
                self._transaction_node.release(self._transaction_connection)
                self._transaction_connection = None

            self._pipe.cluster_client.reinitialize_counter += 1
            if (
                self._pipe.cluster_client.reinitialize_steps
                and self._pipe.cluster_client.reinitialize_counter
                % self._pipe.cluster_client.reinitialize_steps
                == 0
            ):

View on GitHub (pinned to 6a6b581b48)