Fail Fast on Purpose: The Circuit Breaker

The circuit breaker state machine: closed, open, and half-open, with the transitions between them

A dependency goes down. Maybe the payments service is overloaded. Your code calls it, waits for a 30-second timeout, fails, and (if you are unlucky) retries. Now multiply that by your traffic: thousands of goroutines all parked on doomed calls, each holding a connection and a timeout, your own latency climbing, your own health checks starting to fail. A single sick service has made you sick too. This is a cascading failure, and the circuit breaker is the standard guard against it.

The idea is borrowed straight from electrical circuits. When current spikes, the breaker trips and the circuit goes dead, on purpose, to protect everything downstream. You flip it back later once things look safe.

Three states

A software breaker is a little state machine with three states:

func (b *Breaker) Do(fn func() error) error {
    ok, isProbe := b.allow()   // decide under the lock
    if !ok {
        return ErrOpen         // fail fast, do not even call fn
    }

    err := fn()

    b.mu.Lock()
    defer b.mu.Unlock()
    if isProbe {
        b.probing = false
        if err == nil {
            b.state = stateClosed   // recovered
            b.failures = 0
        } else {
            b.state = stateOpen     // still broken, back off
            b.openUntil = time.Now().Add(b.cooldown)
        }
        return err
    }
    // closed-state bookkeeping
    if err == nil {
        b.failures = 0
        return nil
    }
    b.failures++
    if b.failures >= b.maxFailures {
        b.state = stateOpen          // trip
        b.openUntil = time.Now().Add(b.cooldown)
        b.failures = 0
    }
    return err
}

The subtle part: exactly one probe

The transition that people get wrong is half-open. When the cooldown elapses, you do not want to suddenly let all the queued-up traffic through, because if the dependency is still down you just stampeded it again and re-tripped. You want a single, gentle probe.

case stateOpen:
    if time.Now().Before(b.openUntil) {
        return false, false       // still cooling down: reject
    }
    b.state = stateHalfOpen       // cooldown over
    b.probing = true
    return true, true             // admit exactly ONE probe

case stateHalfOpen:
    if b.probing {
        return false, false       // a probe is already out; everyone else waits
    }

The probing flag is the bouncer. The first call after the cooldown becomes the probe and flips probing to true; every other call sees probing is set and gets rejected until the probe resolves. One request decides the fate of the breaker, and the herd is held back until it does.

Why this is the right behavior

This is the pattern behind Hystrix, resilience4j, and the resilience layers of most service meshes. The three states and the single-probe rule are the whole essence; everything else (rolling failure windows, percentages, half-open concurrency limits) is refinement on top.

The mindset shift is the valuable bit: sometimes the most reliable thing your code can do is refuse to try.


Build it yourself. Solve the Circuit Breaker challenge on barehands and get graded on correctness and speed. No libraries, just the standard toolchain.