All guides
No guide matches your search.
Circuit breaker
Configure a circuit breaker, read its seven arguments in order, decide which outcomes count against it, and recognize the refusal you get while it is open.
On this page
Circuit state lives in one PHP process. Under PHP-FPM, a pipeline built inside a request starts every request with a closed circuit, and no request sees the failures of another. See Limits.
The code blocks come from examples/09-choosing-policies.php (opens in a new tab). A script runs it and compares it with the recorded output. The numbers make behavior visible. They are not recommendations.
What a circuit breaker is for
A dependency is down and callers keep hitting it. Every call waits, fails and adds load. The breaker watches the outcomes of recent calls. When too many have failed, it opens and refuses new calls at once, without running your code. After a while it lets a few probe calls through to see whether the dependency has recovered.
Configure it
The snippet opens a circuit after repeated failures and shows what the fifth call gets. It also builds the same breaker over a time window. It starts from this shared setup:
/** Every pipeline below starts from the same two required options. */
$base = static fn(): PipelineBuilder => PipelineBuilder::profile('choosing', 'v1')
->partition('shop', 'prod', 'catalog-api')
->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative);
$ok = static fn(): string => 'ok';
$context = static fn(CompiledPipeline $pipeline): ExecutionContext => $pipeline->newContext(Duration::seconds(30));echo "-- 3. Circuit breaker: count window or time window\n";
$countWindow = PipelineBuilder::lastCalls(4);
$timeWindow = PipelineBuilder::timeWindow(Duration::seconds(60), Duration::seconds(5));
$pipeline = $base()
->circuitBreaker($countWindow, 4, Ratio::of(1, 2), Duration::seconds(30), 1, 1, Duration::seconds(10))
->compileInMemory(ExceptionClassifier::create());
$invoked = 0;
$broken = static function () use (&$invoked): string {
++$invoked;
throw new \DomainException('down');
};
for ($i = 1; $i <= 5; ++$i) {
try {
$pipeline->execute($broken, $context($pipeline));
} catch (\DomainException) {
// Closed circuit: your exception comes through.
} catch (AdmissionRejectedException $e) {
echo "call {$i} refused with {$e->reason()->value}; the callable ran {$invoked} times\n";
}
}
// The same breaker over a 60 s window split into 5 s buckets.
$timeBased = $base()
->circuitBreaker($timeWindow, 4, Ratio::of(1, 2), Duration::seconds(30), 1, 1, Duration::seconds(10))
->compileInMemory(ExceptionClassifier::create());
echo 'time-window circuit compiled: ', $timeBased->explain()->profileId->value, "\n";-- 3. Circuit breaker: count window or time window
call 5 refused with circuit_open; the callable ran 4 times
time-window circuit compiled: choosingWhile the circuit is closed, your own exception comes through unchanged (the empty catch above). Once the circuit is open, Reqtide refuses the call with an AdmissionRejectedException and the reason circuit_open, and the callable no longer runs. It ran four times in total. A refused call never reaches your callable, and the refusal is not a sample.
Read the arguments in order
The signature is circuitBreaker(window, minimumSamples, failureThreshold, openDuration, probeCapacity, probeSuccessThreshold, permitLease).
| Argument | Meaning |
|---|---|
window | lastCalls(n) keeps the last n completed non-neutral outcomes. timeWindow(window, bucketGranularity) adds up outcomes in buckets and is approximate at its boundary. |
minimumSamples | How many outcomes must exist before the circuit may open. It cannot exceed a count window. |
failureThreshold | The failure share at which the circuit opens, as a Ratio. Ratio::of(1, 2) means half. |
openDuration | How long the circuit stays open before it admits probes. |
probeCapacity | How many probe calls may be in flight while the circuit is half-open. |
probeSuccessThreshold | How many successful probes close the circuit. It cannot exceed the probe capacity. |
permitLease | How long the permit of a call stays valid. It must exceed the attempt timeout by more than 1 second. |
Closed, open, half-open
- Closed. Calls pass, and each completed outcome goes into the window. When the window holds at least
minimumSamplesand the share of failures reaches the threshold, the circuit opens. - Open. Reqtide refuses calls with
circuit_open. AfteropenDurationthe circuit becomes half-open. - Half-open. Up to
probeCapacityprobes are admitted. The circuit closes afterprobeSuccessThresholdsuccessful probes. A failed probe reopens it at once. A call refused for lack of probe capacity (circuit_probe_capacity_exhausted) is not a sample.
The circuit is safe against stale or lost results. A probe that is abandoned or that expires frees its slot without counting as a success or a failure. A result that arrives after the circuit reopened cannot change its state. Every permit ends exactly once: on success, on exception and on cancellation. The circuit emits a CircuitTransitioned event on every change.
What counts as a failure
Your classifier decides. With ExceptionClassifier:
- Every exception counts as a circuit failure, unless you list its class in
ignoreForCircuit(). Your own validation error is a good candidate, because it says nothing about the dependency. - Once you give a
countAsCircuitFailure()list, only the listed classes and the retryable ones are failures. Every other exception is circuit-neutral. unacceptableValuesWhen()marks a returned value as a failure, for example an error payload returned with a normal status.- Refusals by other protections, cancellations and storage errors are neutral. They neither open nor heal the circuit.
A call that settles after the operation deadline is neutral too. The caller already got DeadlineExceeded, and the late result says nothing the circuit may act on. Give the callable its own timeout, shorter than the pipeline deadline, so that a slow dependency fails inside the deadline and counts. The circuit does not count slow calls.
An exception nobody listed counts as a failure. A circuit that ignored unknown failures would stay closed against a dependency that fails in a way nobody anticipated.
Testing recovery
A test of "open, wait 30 seconds, probe, close" should not wait 30 seconds. Move a virtual clock instead. See Testing with the virtual clock.
Was this page helpful?