redis/redis-py · error · NoValidDatabaseException

Cannot set active database, database is unhealthy

Error message

Cannot set active database, database is unhealthy

What it means

Raised as NoValidDatabaseException by set_active_database() when the requested Database exists in the list but its circuit breaker is not CLOSED (it is OPEN or HALF_OPEN) after a fresh _check_db_health. The client refuses to route traffic to a database that the health-check layer just confirmed is unhealthy.

Solutions

  1. Wait for the circuit to recover (reset_timeout/grace_period) or run await client._check_db_health(db) and re-check db.circuit.state == CBState.CLOSED before retrying.
  2. Fix the underlying health issue on the target (network, Redis process, lag) then retry set_active_database.
  3. If you must promote now, pick a different healthy database from get_databases() whose circuit is already CLOSED.
  4. Increase health_check_timeout or relax the health-check policy if probes are flapping and falsely opening the circuit.

Example fix

// before
await client.set_active_database(db)  # NoValidDatabaseException: unhealthy

// after
from redis.multidb.circuit import State as CBState
healthy = [d for d, _ in client.get_databases() if d.circuit.state == CBState.CLOSED]
if not healthy:
    raise RuntimeError("no healthy database to promote")
await client.set_active_database(healthy[0])
Defensive patterns

Strategy: validation

Validate before calling

from redis.multidb.circuit import State as CBState

def is_promotable(client, db) -> bool:
    return any(d is db and d.circuit.state == CBState.CLOSED for d, _ in client.get_databases())

if not is_promotable(client, target):
    # wait for recovery or pick another candidate
    ...

Type guard

from redis.multidb.circuit import State as CBState

def circuit_is_closed(db) -> bool:
    return db.circuit.state == CBState.CLOSED

Try / catch

from redis.multidb.exception import NoValidDatabaseException

try:
    await client.set_active_database(target)
except NoValidDatabaseException as e:
    if "unhealthy" in str(e):
        logger.warning("target %s unhealthy; deferring promotion", target)
        # schedule a retry after the circuit reset_timeout
    else:
        raise

Prevention

When it happens

Trigger: Calling await client.set_active_database(db) where db.circuit.state is OPEN/HALF_OPEN at call time — e.g. forcing a manual failover to a database that is currently failing PING, is mid-recovery, or whose LagAwareHealthCheck REST probe is returning errors. The health re-check at client.py:166 runs immediately before the circuit check.

Common situations: Operator-driven failover to a region/endpoint that is still recovering from an outage, lag spikes on an Active-Active replica exceeding lag_aware_tolerance, network blip that opened the circuit, or attempting to repromote a database before the circuit's reset_timeout/grace_period elapsed.

Related errors


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

Appendix: source

Thrown at redis/asyncio/multidb/client.py:175

        for existing_db, _ in self._databases:
            if existing_db == database:
                exists = True
                break

        if not exists:
            raise ValueError("Given database is not a member of database list")

        await self._check_db_health(database)

        if database.circuit.state == CBState.CLOSED:
            highest_weighted_db, _ = self._databases.get_top_n(1)[0]
            await self.command_executor.set_active_database(
                database, GeoFailoverReason.MANUAL
            )
            return

        raise NoValidDatabaseException(
            "Cannot set active database, database is unhealthy"
        )

    async def add_database(
        self, config: DatabaseConfig, skip_initial_health_check: bool = True
    ):
        """
        Adds a new database to the database list.

        Args:
            config: DatabaseConfig object that contains the database configuration.
            skip_initial_health_check: If True, adds the database even if it is unhealthy.
        """
        # The retry object is not used in the lower level clients, so we can safely remove it.
        # We rely on command_retry in terms of global retries.
        config.client_kwargs.update({"retry": Retry(retries=0, backoff=NoBackoff())})

        if config.from_url:

View on GitHub (pinned to 6a6b581b48)