# Circuit Breaker — Part 1: States and Why It Exists

This topic assumes you've read `06-retry_with_exponential_backoff/`. Retry with backoff answers "how should ONE call handle a transient failure of the thing it's calling." A circuit breaker answers a related but different question: "once a downstream dependency is CONSISTENTLY failing, how do we stop every caller from continuing to hammer it at all — retries included." Part 2 covers half-open probe mechanics in depth, combining circuit breaking with retry/backoff, and bulkheading.

## 1. The Problem Retry Alone Doesn't Solve

Retry with backoff makes each individual failing call more polite about retrying. But if a downstream service is fully down — not flaky, just down — every caller, even with perfect backoff and jitter, is still eventually going to retry it, over and over, forever, each retry costing a connection attempt, a timeout to expire, resources on both sides. Multiply that by every caller in the system, and a downstream outage keeps consuming capacity on the *calling* side too, and keeps applying load to a downstream service that needs to be left alone to recover, not continuously probed by everyone.

```
Downstream service goes down at t=0.
Without a circuit breaker:
  Every caller keeps calling (with backoff, so not instantly, but still --
  forever) -> every call still pays a full connection/timeout cost ->
  callers' own resources (threads, connections) get tied up waiting on a
  dependency that has no chance of responding -> the FAILURE SPREADS to
  services that depend on the caller, even though those services have
  nothing to do with the original outage.
```

This upstream resource exhaustion — not the original outage itself — is very often what actually takes down a system during a dependency failure. A circuit breaker's job is to stop that spread by making the caller stop trying, fast, once it's clear the downstream is not going to succeed.

## 2. The Three States

A circuit breaker wraps calls to a dependency and tracks their outcomes, moving between three states:

```
        failures exceed threshold
  CLOSED ─────────────────────────▶ OPEN
    ▲                                 │
    │                                 │ after a cooldown timeout
    │ probe succeeds                  ▼
    └──────────────────────── HALF-OPEN
                probe fails
              (back to OPEN)
```

- **Closed** — the normal state. Calls pass through to the real dependency. The breaker counts recent failures.
- **Open** — the breaker has seen enough failures to conclude the dependency is down. Calls **fail immediately, without even attempting the real call** — this is the entire point: stop spending resources on a call that's overwhelmingly likely to fail anyway.
- **Half-open** — after a cooldown period, the breaker allows a small number of "probe" calls through to test whether the dependency has recovered. Success moves back to Closed; failure moves back to Open (and resets the cooldown).

```ts
type CircuitState = "closed" | "open" | "half-open";

interface Breaker {
  state: CircuitState;
  consecutiveFailures: number;
  failureThreshold: number;   // e.g. 5 -- trip to OPEN after 5 in a row
  openedAt: number | null;    // timestamp, used to know when cooldown elapses
  cooldownMs: number;         // e.g. 30000 -- how long to stay OPEN before probing
}
```

## 3. Closed → Open: Tripping the Breaker

While closed, every failed call increments a failure counter; every successful call resets it. Once consecutive (or windowed — Part 2 covers the distinction) failures cross `failureThreshold`, the breaker trips to **open** and remembers when:

```ts
function recordFailure(breaker: Breaker, now: number): void {
  breaker.consecutiveFailures++;
  if (breaker.state === "closed" && breaker.consecutiveFailures >= breaker.failureThreshold) {
    breaker.state = "open";
    breaker.openedAt = now;
  }
}
```

Once open, calls are rejected **without attempting the real dependency at all**:

```ts
function call<T>(breaker: Breaker, fn: () => T, now: number): T {
  if (breaker.state === "open") {
    if (now - (breaker.openedAt ?? 0) < breaker.cooldownMs) {
      throw new Error("circuit open — failing fast, not calling downstream");
    }
    breaker.state = "half-open"; // cooldown elapsed, allow a probe through
  }
  try {
    const result = fn();
    recordSuccess(breaker);
    return result;
  } catch (err) {
    recordFailure(breaker, now);
    throw err;
  }
}
```

This is the core mechanical difference from retry: retry still calls the real dependency, just with delay between attempts. An open circuit breaker **doesn't call the dependency at all** — it fails locally and instantly, which is exactly what stops the caller-side resource exhaustion described in Section 1.

![Closed calls pass through and count failures; crossing the threshold trips to Open, which fails fast with no real call; after a cooldown a single probe in Half-open decides whether to return to Closed or back to Open](diagrams/01-circuit-states.svg)

## 4. Half-Open: Testing Recovery Without Fully Committing

Going straight from Open back to Closed after a fixed timeout would be guessing blind — the dependency might still be down. Half-open lets through a **small number of probe calls** (often just one) to actually test the water:

- Probe **succeeds** -> the dependency is very likely back -> transition to **Closed**, reset the failure counter.
- Probe **fails** -> still down -> transition back to **Open**, restart the cooldown timer.

Crucially, while half-open, the breaker does **not** let the full flood of normal traffic through — only the limited probe volume — so a still-broken dependency isn't immediately re-hammered by every caller the moment the cooldown expires.

## Common Pitfalls

1. **Treating retry and circuit breaking as interchangeable** — retry handles a single call's transient blip; a circuit breaker handles a *sustained* failure by stopping calls altogether. They solve different problems and are normally used together (Part 2).
2. **No half-open state at all** (binary open/closed with a timer) — either keeps failing fast forever with no way back to closed, or flips straight back to closed and gets immediately re-tripped by full traffic against a dependency that's still down.
3. **Letting the full traffic volume through during half-open** — defeats the purpose of probing cautiously; a single bad probe request among thousands re-floods a barely-recovering dependency.
4. **Sizing `failureThreshold` too low** — trips the breaker (and starts failing fast for ALL callers) on normal, brief error-rate noise rather than a genuine sustained outage.
5. **No cooldown, or a cooldown so short it barely differs from immediate retry** — reopens the door to the dependency before it's had any real time to recover.

## Quick Reference

```
States: CLOSED (normal, counts failures) -> OPEN (fail fast, NO real call) -> HALF-OPEN (probe) -> CLOSED or OPEN

Open circuit = calls fail LOCALLY and INSTANTLY -- the defining difference from retry,
               which still calls the real dependency, just with delay

Trip condition: consecutive/windowed failures >= failureThreshold  ->  CLOSED to OPEN
Recovery test:  after cooldownMs elapses, OPEN -> HALF-OPEN, allow limited probe traffic
Probe succeeds -> HALF-OPEN -> CLOSED (reset failure count)
Probe fails    -> HALF-OPEN -> OPEN (restart cooldown)
```

Continue to `dist_circuit_breaker_theory_2.md` for windowed vs consecutive failure counting, combining circuit breaking with retry/backoff, and bulkheading alongside it.
