webrek/laravel-circuit-breaker
Stop a failing dependency from dragging your app down with it. Wrap calls to an external service in a circuit breaker: after enough failures it trips open and fails fast — so requests stop piling up on a downed endpoint — then probes for recovery and closes itself once the service is healthy again.
When a downstream service (a payment gateway, a partner API, a webhook endpoint) gets slow or goes down, every request to it hangs until it times out. Those requests pile up, exhaust your workers and connection pool, and their outage becomes yours: a cascading failure. A circuit breaker watches for failures and, once they cross a threshold, cuts off subsequent calls during a cooldown so your app keeps responding while the dependency recovers.
use Webrek\CircuitBreaker\Facades\CircuitBreaker;
$response = CircuitBreaker::for('payments')->call(
fn () => Http::timeout(3)->post($url, $payload)->throw(),
fallback: fn () => null, // returned while the circuit is open
);
composer require webrek/laravel-circuit-breaker
Optionally publish the config:
php artisan vendor:publish --tag=circuit-breaker-config
State is stored in the cache. In production, point it at a centralized store (Redis) so the breaker is shared across every process and server: the array and file drivers only protect a single process.
A circuit moves between three states:
failure_threshold, the circuit trips open.CircuitOpenException)
without touching the dependency. After cooldown_seconds, the next call is
allowed as a trial: half-open.success_threshold consecutive
successes close the circuit; a single failure trips it open again.When a fallback is provided, an open circuit (or a failing call) returns it
instead of throwing. The fallback receives the Throwable:
$rate = CircuitBreaker::for('fx-api')->call(
fn () => $this->fetchLiveRate(),
fallback: fn (\Throwable $e) => $this->lastKnownRate(),
);
Omit it and the breaker rethrows the underlying exception — or throws
CircuitOpenException while open — for you to handle yourself:
use Webrek\CircuitBreaker\Exceptions\CircuitOpenException;
try {
CircuitBreaker::for('payments')->call(fn () => $gateway->charge($order));
} catch (CircuitOpenException $e) {
return back()->withErrors('Payments are temporarily unavailable.');
}
A 422 from a validation error means your request was wrong, not that the
service is down: it shouldn't open the breaker. List those exceptions under
ignore and they pass through without affecting the circuit:
// config/circuit-breaker.php
'defaults' => [
'ignore' => [
Illuminate\Http\Client\RequestException::class, // only if you treat 4xx as a caller error
],
],
The relay in webrek/laravel-outbox can deliver through a breaker so it stops retrying against an already-downed endpoint, and resumes automatically once it recovers.
Lifecycle events let you alert on state changes:
| Event | When |
|---|---|
CircuitOpened |
A circuit tripped open. |
CircuitHalfOpened |
An open circuit started a recovery trial. |
CircuitClosed |
A circuit recovered. |
Force a circuit closed by hand:
php artisan circuit-breaker:reset payments
Inspect state in code with CircuitBreaker::for('payments')->state() and
->available().
return [
'cache' => [
'store' => env('CIRCUIT_BREAKER_CACHE'), // null = default; use Redis in production
'prefix' => 'circuit-breaker',
'ttl' => 86400,
],
'defaults' => [
'failure_threshold' => 5, // consecutive failures that open the circuit
'cooldown_seconds' => 30, // open → half-open after this
'success_threshold' => 1, // trial successes needed to close
'ignore' => [], // exceptions that don't count as failures
],
'circuits' => [
'payments' => ['failure_threshold' => 3, 'cooldown_seconds' => 60],
],
];
| Component | Version |
|---|---|
| PHP | 8.2+ |
| Laravel | 12.x / 13.x |
| Cache | A shared store (Redis) in production |
composer install
composer test
See CONTRIBUTING.md.
Please review the security policy before reporting a vulnerability.
Released under the MIT License.
How can I help you explore Laravel packages today?