redis/redis-py · error · ConnectionError

READONLY command failed

Error message

READONLY command failed

What it means

Raised in RedisCluster.on_connect after sending READONLY and reading the reply: if the reply is not the string 'OK' the connection is considered misconfigured for replica reads and ConnectionError is raised. on_connect runs for every new connection when read_from_replicas or load_balancing_strategy is set, so this surfaces at first use of each connection.

Solutions

  1. Only enable read_from_replicas/load_balancing_strategy against a real cluster with replicas.
  2. Grant the connecting ACL user permission to issue READONLY.
  3. If you do not need replica reads, leave read_from_replicas=False (default).

Example fix

// before
client = RedisCluster(host=..., port=..., read_from_replicas=True)
// after
# only enable if the topology has replicas and the ACL permits READONLY
client = RedisCluster(host=..., port=..., read_from_replicas=False)
# or, with replicas:
client = RedisCluster(host=..., port=..., read_from_replicas=True)  # and fix ACL
Defensive patterns

Strategy: validation

Validate before calling

read_from_replicas = read_from_replicas and bool(client.get_replicas())
# Or statically: only enable read_from_replicas for known-replica topologies.

Type guard

def cluster_supports_readonly(client) -> bool:
    try:
        return len(client.get_replicas()) > 0
    except Exception:
        return False

Try / catch

from redis.exceptions import ConnectionError
try:
    await client.set('k', 'v')
except ConnectionError as e:
    if 'READONLY command failed' in str(e):
        client = RedisCluster(host=..., port=..., read_from_replicas=False)
        await client.set('k', 'v')
    else:
        raise

Prevention

When it happens

Trigger: Constructing RedisCluster(..., read_from_replicas=True) (or load_balancing_strategy=...) and connecting to a node that rejects READONLY — e.g. the node is a primary that disallows READONLY, the ACL user lacks permission, or the server is a proxy that returns an unexpected reply.

Common situations: Pointing read_from_replicas=True at a non-cluster or single-node setup where READONLY is meaningless; ACL restrictions on the connecting user; a managed Redis that does not support READONLY.

Related errors


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

Appendix: source

Thrown at redis/asyncio/cluster.py:787

        if hasattr(self, "_initialize") and not self._initialize:
            _warn(f"{self._DEL_MESSAGE} {self!r}", ResourceWarning, source=self)
            try:
                context = {"client": self, "message": self._DEL_MESSAGE}
                _grl().call_exception_handler(context)
            except RuntimeError:
                pass

    async def on_connect(self, connection: Connection) -> None:
        await connection.on_connect()

        # Sending READONLY command to server to configure connection as
        # readonly. Since each cluster node may change its server type due
        # to a failover, we should establish a READONLY connection
        # regardless of the server type. If this is a primary connection,
        # READONLY would not affect executing write commands.
        await connection.send_command("READONLY")
        if str_if_bytes(await connection.read_response()) != "OK":
            raise ConnectionError("READONLY command failed")

    def get_nodes(self) -> List["ClusterNode"]:
        """Get all nodes of the cluster."""
        return list(self.nodes_manager.nodes_cache.values())

    def get_primaries(self) -> List["ClusterNode"]:
        """Get the primary nodes of the cluster."""
        return self.nodes_manager.get_nodes_by_server_type(PRIMARY)

    def get_replicas(self) -> List["ClusterNode"]:
        """Get the replica nodes of the cluster."""
        return self.nodes_manager.get_nodes_by_server_type(REPLICA)

    def get_random_node(self) -> "ClusterNode":
        """Get a random node of the cluster."""
        return random.choice(list(self.nodes_manager.nodes_cache.values()))

    def get_default_node(self) -> "ClusterNode":

View on GitHub (pinned to 6a6b581b48)