The Go library circuit from cep21, tagged v4.2.0, was published on 18 August 2026. The release is a bug fix and performance pass over the open and half-open state machine and over rolling window statistics. The change operators will feel first is ForceOpen. It now rejects every request, including the half-open probes that previously still ran.
The full release notes and source archives are on the GitHub release page.
ForceOpen now rejects probes ¶
Manual open was leaky. With ForceOpen set, the breaker still admitted half-open probes: one per SleepWindow, or every request if the closer was permissive. A drain that was supposed to shed all outbound calls still let a trickle through. That trickle is gone. Every request is rejected while forced open.
OpenCircuit(), CloseCircuit(), and automatic closes now act on the underlying state even when ForceOpen or ForcedClosed is set. Those calls used to do nothing. Flipping a force flag and then calling CloseCircuit() did not restore the machine.
Metrics.Opened and Metrics.Closed are delivered exactly once, strictly alternating, in transition order, and without locks held. A metrics listener that calls back into the circuit no longer deadlocks. A slow or panicking listener no longer wedges later deliveries. That matters if your listener records a trip and then opens a sibling breaker on the same process.
The timeout race is the one that will match production pages. ExecutionTimeout is read once per run. A concurrent SetConfigThreadSafe could previously build a deadline already in the past, then emit ErrTimeout and open a healthy circuit. Config reloads during a run are now safe on that path. An open circuit already at MaxConcurrentRequests also short circuits without burning the closer’s half-open permit, so a request that was going to be shed does not consume the probe slot.
Rolling stats after quiet windows ¶
Pipeline workers often sit idle longer than one rolling window between batches. RollingBuckets.Advance used to publish a new index first, then clear expired buckets with a lock free walk. A burst of writers after that idle gap could land in a bucket that was then wiped. The opener and RunStats undercounted the first requests after more than one window of quiet. Error rate could look cleaner than it was, right when a job started hammering a bad endpoint.
The fix clears expired buckets before publishing the new index.
RollingCounter now derives its rolling sum from the buckets. The sum is never negative. Inc does one fewer contended atomic. JSON output is unchanged, so scrapers of the old shape keep working.
RollingPercentile snapshots no longer include durations from an expired window. On the benchstat run with -cpu=8, snapshot time dropped 71 percent and allocs went from 13 to 1.
Manager.CreateCircuit now checks for duplicates before running DefaultCircuitProperties. A failed duplicate create used to leak rolling.StatFactory entries. The manager also drops its lock before user constructors run, so a constructor that calls GetCircuit no longer deadlocks. Partial rolling Run and Fallback configs fill defaults instead of dividing by zero.
Probe window on the hystrix closer ¶
The closers/hystrix Closer now honors SleepWindow. Probes are admitted only after the window armed by Opened(). Only requests that started after the circuit opened count toward closing it.
The old behavior closed the circuit on any in flight success, including a call that began before the open. A stale in flight failure could also void a healthy probe. Timeout plus retry plus a late body is a common pattern for outbound HTTP in ETL and streaming workers. That pattern could flap the breaker closed, then open again, with no new evidence.
Closer.SetConfigThreadSafe no longer races on TimeAfterFunc. Opener.ShouldOpen switched to integer math, so an error rate sitting exactly on the threshold now trips. The notes give 57/100 versus 57 percent as the example. Opener.SetConfigNotThreadSafe with unset NumBuckets no longer panics.
Upgrade notes ¶
The module requires Go 1.24 or newer. CI covers 1.24, 1.25, and 1.26. The go.mod directive was raised to 1.24.
The public API has no breaking changes. That was checked with gorelease. The only addition is SimpleBadRequest.Unwrap.
Do not treat “no breaking API” as “no behavior change”. Several fixes change what you will see in metrics and in tests. Read the behavior changes in v4.2 before upgrading, especially if you use ForceOpen, drive a hystrix.Closer directly in tests, or implement custom ClosedToOpen or OpenToClosed logic.
Shed and error paths (open, failure plus fallback, throttled fallback) take 30 to 48 percent less time and drop to 0 allocs from 1 to 3. The parallel success path takes 37 to 45 percent less time. Per request counters sit on their own cache line, and Closer.Success no longer does an atomic RMW while closed. IsBadRequest went from 75 to 150 ns and 1 alloc down to 2 to 5 ns and 0 allocs. Those numbers are from the notes, benchstat with -cpu=8.
The stretchr/testify bump to v1.12.0 and the CI action bumps are test toolchain only. The commit range is the v4.1.0 to v4.2.0 compare.
Where to get it ¶
- The v4.2.0 GitHub release page
- The circuit repository
- Tag
v4.2.0