redis/redis-py · warning · UnhealthyDatabaseException

Unhealthy database

Error message

Unhealthy database

What it means

Raised as UnhealthyDatabaseException by AbstractHealthCheckPolicy.execute() (healthcheck.py:153-156) when one of the concurrent health-check probe tasks returned an Exception (including asyncio.TimeoutError from the per-check timeout) instead of a clean True/False. It carries the offending database and the original exception so callers can mark the circuit OPEN.

Solutions

  1. Inspect exception.original_exception and exception.database on the caught UnhealthyDatabaseException to localize the failure.
  2. Increase health_check_timeout if probes are timing out on slow links, or health_check_probes for noisy networks (with a majority/any policy).
  3. Fix the underlying connectivity (network/TLS/Redis process/REST API) for the affected database.
  4. Switch health_check_policy to HEALTHY_MAJORITY or HEALTHY_ANY so a single probe exception does not open the circuit.
  5. Confirm LagAwareHealthCheck has health_check_url set and the Redis Enterprise REST API credentials/TLS are correct.

Example fix

// before - default HEALTHY_ALL opens the circuit on any probe exception
config = MultiDbConfig(
    databases_config=dbs,
    health_check_policy=HealthCheckPolicies.HEALTHY_ALL,
)

// after - tolerate transient probe failures
from redis.asyncio.multidb.healthcheck import HealthCheckPolicies
config = MultiDbConfig(
    databases_config=dbs,
    health_check_policy=HealthCheckPolicies.HEALTHY_MAJORITY,
    health_check_timeout=6.0,
)
Defensive patterns

Strategy: try-catch

Validate before calling

from redis.multidb.circuit import State as CBState

def databases_look_healthy(client) -> bool:
    return all(d.circuit.state == CBState.CLOSED for d, _ in client.get_databases())

# before a latency-sensitive op, optionally check circuit states;
# UnhealthyDatabaseException is raised internally and flips circuits OPEN,
# so the caller mainly observes it via the circuit state / failover path.

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 UnhealthyDatabaseException

try:
    await client._check_db_health(db)
except UnhealthyDatabaseException as e:
    logger.warning(
        "database %s unhealthy: %r", e.database, e.original_exception
    )
    # the circuit is now OPEN; failover will pick another DB

Prevention

When it happens

Trigger: A health check (PingHealthCheck/LagAwareHealthCheck/custom) raising — e.g. PING hitting ConnectionRefusedError/TimeoutError, LagAwareHealthCheck failing to reach the REST API, a custom HealthCheck.check_health raising. _check_db_health catches it and flips the circuit OPEN via _check_databases_health.

Common situations: Network partition or Redis process down causing PING to time out (exceeding health_check_timeout), LagAwareHealthCheck's REST call failing (auth, TLS, 9443 unreachable), DNS resolution failure mid-probe, or a flapping link causing intermittent probe exceptions.

Related errors


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

Appendix: source

Thrown at redis/asyncio/multidb/healthcheck.py:156

        # Create wrapper tasks that apply individual timeouts
        async def execute_with_timeout(health_check: HealthCheck):
            return await asyncio.wait_for(
                self._execute(health_check, database),
                timeout=health_check.health_check_timeout,
            )

        # Run all health checks concurrently and collect results/exceptions
        results = await asyncio.gather(
            *[execute_with_timeout(hc) for hc in health_checks],
            return_exceptions=True,
        )

        # Check results - handle exceptions and failures
        for result in results:
            if isinstance(result, Exception):
                # Any exception (including TimeoutError) makes the database unhealthy
                raise UnhealthyDatabaseException("Unhealthy database", database, result)
            elif not result:
                # Health check returned False
                return False

        return True

    async def get_client(self, database) -> AsyncRedisClientT:
        """
        Get or create a health check client for the database.

        Creates a single client instance per database that follows topology
        changes automatically. For cluster databases, the client handles
        node discovery and slot mapping internally.
        """
        db_id = id(database)
        client = self._clients.get(db_id)

        if client is None:

View on GitHub (pinned to 6a6b581b48)