Patterns: concurrent Go · 3 MIN READ

Circuit breaker: stop calls while a dependency recovers

Separate retry policy from failure isolation and a controlled recovery probe.

Separate retry policy from failure isolation and a controlled recovery probe. A circuit breaker rejects calls after repeated failures, waits for a recovery interval, then permits a probe. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.

Concurrent Go pattern · A MAP OF THE IDEA

  1. Closed state observes counted failures
  2. Open state rejects calls for an interval
  3. Half-open probe succeeds or reopens the circuit

Useful when

  • A failing dependency consumes caller capacity
  • Recovery probes should be controlled

Watch out

  • Concurrent state and probe admission need protection
  • Failure classification and metrics affect usefulness

Implementation & operation cost: High

Fit depends on the task

Small tool or CLI2/5
Domain-rich application4/5
Independent distributed services5/5

Editorial suitability, not benchmarks or a universal ranking. Styles describe different axes and can be combined.

01

The design decision

A circuit breaker rejects calls after repeated failures, waits for a recovery interval, then permits a probe. Closed, open and half-open describe the lifecycle. Unlike a retry, it reduces attempted work during a continuing outage; it does not prove the dependency is healthy.

02

A complete Go example

Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.

go
package main

import (
	"errors"
	"fmt"
	"time"
)

var ErrOpen = errors.New("circuit open")

type Breaker struct {
	failures int
	until    time.Time
}

func (b *Breaker) Call(now time.Time, op func() error) error {
	if now.Before(b.until) {
		return ErrOpen
	}
	err := op()
	if err == nil {
		b.failures = 0
		b.until = time.Time{}
		return nil
	}
	b.failures++
	if b.failures >= 2 {
		b.until = now.Add(time.Second)
	}
	return err
}

func main() {
	b := &Breaker{}
	now := time.Unix(0, 0)
	calls := 0
	fail := func() error { calls++; return errors.New("offline") }
	_ = b.Call(now, fail)
	_ = b.Call(now, fail)
	fmt.Println(b.Call(now, fail), calls)
	fmt.Println(b.Call(now.Add(time.Second), func() error { return nil }))
}

// Output:
// circuit open 2
// <nil>
Read the code

After two failures, a third call is rejected without invoking fail. Advancing the injected clock permits a successful recovery probe and resets the count. This is a single-owner teaching breaker; sequential execution permits one probe at a time implicitly.

03

Tradeoffs and the simpler alternative

A concurrent implementation must lock state and admit only a bounded number of half-open probes. Classify which failures count, expose metrics, and avoid treating authorization errors as an outage. Prefer a proven library when failure windows and concurrent recovery become complex.

TAKE THIS WITH YOU

Rejects calls during an outage and probes for recovery. A concurrent implementation must lock state and admit only a bounded number of half-open probes. Classify which failures count, expose metrics, and avoid treating authorization errors as an outage. Prefer a proven library when failure windows and concurrent recovery become complex.

Check your understanding

Which statement best describes Circuit breaker: stop calls while a dependency recovers?

Go to the source

Progress stays in this browser. No account needed.