A circuit breaker needs shared state and a fail-open default

Counting failures in a PHP object means each of fifty workers keeps its own count and opens independently, which is fifty times more slow calls than intended.

private function recordFailure(): void
{
    $key   = "cb:{$this->service}:failures";
    $count = $this->redis->incr($key);

    if ($count === 1) {
        $this->redis->expire($key, $this->windowSeconds);
    }

    if ($count >= $this->threshold) {
        $this->redis->setex("cb:{$this->service}:open", $this->cooldown, 1);
    }
}

Using an expiring key as the open state removes the need for a timer: the circuit closes when the key expires and half-open is simply the first call after it has. The breaker now depends on Redis, so the state check needs a short timeout and a fail-open default — if the bookkeeping store is unreachable, pass the call through. A breaker that blocks traffic because its own dependency is down has become the outage.