{"record":{"id":"592b7d9c2c5d9b4a","repo":"redis/redis-py","slug":"cluster-client-has-no-nodes-cannot-create-health","errorCode":null,"errorMessage":"Cluster client has no nodes - cannot create health check client","messagePattern":"Cluster client has no nodes - cannot create health check client","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"redis/asyncio/multidb/healthcheck.py","lineNumber":209,"sourceCode":"                    # different names (``_require_full_coverage`` vs\n                    # ``require_full_coverage``), so resolve it defensively to\n                    # support a sync RedisCluster underlying client too.\n                    require_full_coverage = getattr(\n                        nodes_manager,\n                        \"require_full_coverage\",\n                        getattr(nodes_manager, \"_require_full_coverage\", True),\n                    )\n                    client = AsyncRedisCluster(\n                        host=first_node.host,\n                        port=first_node.port,\n                        dynamic_startup_nodes=nodes_manager._dynamic_startup_nodes,\n                        address_remap=nodes_manager.address_remap,\n                        require_full_coverage=require_full_coverage,\n                        retry=database.client.retry,\n                        **filtered_kwargs,\n                    )\n                else:\n                    raise ValueError(\n                        \"Cluster client has no nodes - cannot create health check client\"\n                    )\n            else:\n                raise TypeError(f\"Unsupported client type: {type(database.client)}\")\n            self._clients[db_id] = client\n\n        return client\n\n    async def close(self) -> None:\n        \"\"\"Close all health check clients.\"\"\"\n        close_tasks = [\n            asyncio.create_task(client.aclose()) for client in self._clients.values()\n        ]\n\n        if close_tasks:\n            await asyncio.gather(*close_tasks, return_exceptions=True)\n\n        self._clients.clear()","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/asyncio/multidb/healthcheck.py#L191-L227","documentation":"Raised as ValueError by AbstractHealthCheckPolicy.get_client() (healthcheck.py:208-211) when the database's underlying client is a RedisCluster but its startup_nodes collection is empty, so there is no host/port to seed the health-check cluster client from. The health-check layer cannot construct a probe client without at least one known node.","triggerScenarios":"Configuring a DatabaseConfig with a cluster client (client_class=RedisCluster or a cluster URL) that was instantiated without usable startup nodes — e.g. constructed with an empty/malformed host list, a NodesManager that produced no startup nodes, or a cluster client that failed initial discovery and was then handed to MultiDBClient.","commonSituations":"Building RedisCluster from kwargs that omit host/port, pointing at a cluster URL whose CLUSTER NODES discovery returned nothing, race where the cluster client is passed to MultiDBClient before its node discovery completes, or a stale/closed cluster client whose startup_nodes were cleared.","solutions":["Ensure the cluster DatabaseConfig provides a reachable seed host/port (via from_url with a valid redis://cluster-host:port or host/port in client_kwargs).","Verify the cluster is up and CLUSTER NODES returns entries before constructing MultiDBClient.","Construct the RedisCluster client separately, confirm client.startup_nodes is non-empty, then pass it via DatabaseConfig.client_kwargs or a custom Database.","Add a startup assertion: assert client.startup_nodes, 'cluster has no startup nodes'."],"exampleFix":"// before\ndb_cfg = DatabaseConfig(client_kwargs={\"host\": \"\"}, )  # cluster, no seed node\n# or a cluster whose discovery yielded nothing\n\n// after - supply a valid cluster seed URL\nfrom redis.asyncio import RedisCluster\nrc = RedisCluster.from_url(\"redis://cluster-seed.local:6379\")\nassert rc.startup_nodes  # fail fast with a clear cause\ndb_cfg = DatabaseConfig(client_kwargs={\"startup_nodes\": rc.startup_nodes})","handlingStrategy":"validation","validationCode":"from redis.asyncio import RedisCluster\n\ndef cluster_has_startup_nodes(client_cls, kwargs, url) -> bool:\n    rc = RedisCluster.from_url(url) if url else RedisCluster(**kwargs)\n    return bool(rc.startup_nodes)\n\n# before constructing MultiDBClient with a cluster DB:\nassert cluster_has_startup_nodes(cfg.client_class, cfg.client_kwargs, cfg.from_url), \\\n    \"cluster database has no startup nodes\"","typeGuard":"from redis.asyncio import RedisCluster\n\ndef has_seed_nodes(rc: RedisCluster) -> bool:\n    return isinstance(rc, RedisCluster) and bool(rc.startup_nodes)","tryCatchPattern":"try:\n    await client.initialize()\nexcept ValueError as e:\n    if \"no nodes\" in str(e):\n        # fix the cluster seed host/port in DatabaseConfig and reconstruct\n        raise RuntimeError(\"cluster DatabaseConfig needs a reachable seed host/port\")\n    raise","preventionTips":["Provide a reachable seed host/port for every cluster DatabaseConfig.","Verify CLUSTER NODES returns entries before constructing MultiDBClient.","Construct RedisCluster separately and assert startup_nodes is non-empty first.","Do not pass a closed/stale cluster client whose nodes were cleared."],"tags":["multidb","health-check","cluster","config","validation"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}