{"record":{"id":"195a00d68a091eac","repo":"redis/redis-py","slug":"the-previous-master-is-now-a-slave-195a00","errorCode":null,"errorMessage":"The previous master is now a slave","messagePattern":"The previous master is now a slave","errorType":"exception","errorClass":"ConnectionError","httpStatus":null,"severity":"warning","filePath":"redis/sentinel.py","lineNumber":91,"sourceCode":"        disconnect_on_error: Optional[bool] = False,\n        push_request: Optional[bool] = False,\n    ):\n        try:\n            return super().read_response(\n                disable_decoding=disable_decoding,\n                timeout=timeout,\n                disconnect_on_error=disconnect_on_error,\n                push_request=push_request,\n            )\n        except ReadOnlyError:\n            if self.connection_pool.is_master:\n                # When talking to a master, a ReadOnlyError when likely\n                # indicates that the previous master that we're still connected\n                # to has been demoted to a slave and there's a new master.\n                # calling disconnect will force the connection to re-query\n                # sentinel during the next connect() attempt.\n                self.disconnect()\n                raise ConnectionError(\"The previous master is now a slave\")\n            raise\n\n\nclass SentinelManagedSSLConnection(SentinelManagedConnection, SSLConnection):\n    pass\n\n\nclass SentinelConnectionPoolProxy:\n    def __init__(\n        self,\n        connection_pool,\n        is_master,\n        check_connection,\n        service_name,\n        sentinel_manager,\n    ):\n        self.connection_pool_ref = weakref.ref(connection_pool)\n        self.is_master = is_master","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/sentinel.py#L73-L109","documentation":"Raised by SentinelManagedConnection.read_response() (redis/sentinel.py:91) when a command sent on a connection believed to be the master returns redis.exceptions.ReadOnlyError. Redis returns ReadOnlyError ('You can't write against a read only slave') when a node has been demoted to a replica, so receiving it on a master pool means a Sentinel failover happened under us: the node we held a master connection to is now a replica. redis-py disconnects the stale connection (forcing Sentinel re-discovery on the next connect()) and re-raises as ConnectionError so the caller can retry against the newly elected master.","triggerScenarios":"Using a master_for(service_name) client from redis.sentinel.Sentinel, a write command (SET/DEL/INCR/etc.) is dispatched; meanwhile Sentinel has failed over and the old master is now a replica; the in-flight master connection receives ReadOnlyError from Redis; the `if self.connection_pool.is_master` branch at sentinel.py:84 fires, the connection is disconnected, and ConnectionError is raised.","commonSituations":"During or immediately after a Sentinel failover; long-lived / idle connections in the pool that are not recycled before the failover completes; aggressive socket_keepalive keeping stale master connections open; writes issued inside the window between failover start and pool reset.","solutions":["Retry the operation: on the next connect() the pool re-queries Sentinel and binds to the new master.","Wrap write commands in a bounded retry loop with backoff that catches ConnectionError.","Shorten socket_timeout / connection recycling so stale master connections are dropped quickly after a failover.","Check Sentinel health with `SENTINEL masters` / `SENTINEL ckquorum` if the error recurs — repeated occurrences indicate failover instability."],"exampleFix":"# before\nmaster = sentinel.master_for('mymaster')\nmaster.set('k', 'v')  # may raise ConnectionError: The previous master is now a slave\n\n# after: bounded retry on failover-induced ConnectionError\nimport redis, time\nfor attempt in range(5):\n    try:\n        master.set('k', 'v'); break\n    except redis.ConnectionError:\n        time.sleep(0.1 * (2 ** attempt))\nelse:\n    raise","handlingStrategy":"retry","validationCode":"def master_address_stable(sentinel, service_name, prev=None) -> bool:\n    # returns True when Sentinel reports a master address and it is not\n    # in the middle of a failover; call before issuing a burst of writes\n    try:\n        addr = sentinel.discover_master(service_name)\n    except Exception:\n        return False\n    return prev is None or addr == prev","typeGuard":null,"tryCatchPattern":"import redis, time\n\ndef write_with_failover_retry(master_client, op, *args, retries=5, **kw):\n    last = None\n    for attempt in range(retries):\n        try:\n            return op(master_client, *args, **kw)\n        except redis.ConnectionError as e:\n            last = e\n            if 'previous master is now a slave' not in str(e).lower():\n                raise\n            time.sleep(0.1 * (2 ** attempt))\n    raise last\n\n# usage:\n# write_with_failover_retry(master, lambda c, k, v: c.set(k, v), 'k', 'v')","preventionTips":["Wrap Sentinel writes in a bounded retry loop that tolerates this ConnectionError.","Keep socket_timeout and connection recycling short so stale master connections are dropped quickly after failover.","Monitor Sentinel for repeated failovers (`SENTINEL masters`) — flapping indicates an underlying instability to fix."],"tags":["sentinel","failover","replication","connection","high-availability"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}