webrek / laravel-circuit-breaker
A circuit breaker for Laravel: stop hammering a failing dependency, fail fast, and recover automatically.
Requires
- php: ^8.2
- illuminate/cache: ^12.0 || ^13.0
- illuminate/console: ^12.0 || ^13.0
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- infection/infection: ^0.29
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0 || ^11.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0
README
Evita que una dependencia con fallas arrastre a tu aplicación con ella. Envuelve las llamadas a un servicio externo en un circuit breaker: tras suficientes fallas se abre (trips open) y falla rápido —para que no se acumulen más solicitudes sobre un endpoint caído— y luego sondea para detectar la recuperación y se cierra a sí mismo cuando el servicio vuelve a estar sano.
Por qué
Cuando un servicio aguas abajo (una pasarela de pagos, una API de un socio, un endpoint de webhook) se vuelve lento o se cae, cada solicitud hacia él se queda esperando hasta agotar el tiempo de espera. Esas solicitudes se acumulan, agotan tus workers y el pool de conexiones, y su caída se convierte en tu caída: una falla en cascada. Un circuit breaker vigila las fallas y, una vez que cruzan un umbral (threshold), corta en seco las llamadas siguientes durante un cooldown para que tu aplicación siga respondiendo mientras la dependencia se recupera.
use Webrek\CircuitBreaker\Facades\CircuitBreaker; $response = CircuitBreaker::for('payments')->call( fn () => Http::timeout(3)->post($url, $payload)->throw(), fallback: fn () => null, // se devuelve mientras el circuito está abierto );
Instalación
composer require webrek/laravel-circuit-breaker
Opcionalmente publica la configuración:
php artisan vendor:publish --tag=circuit-breaker-config
El estado se guarda en el cache. En producción apúntalo a un almacén centralizado (Redis) para que el breaker se comparta entre todos los procesos y servidores: los drivers de arreglo y archivo solo protegen a un único proceso.
Cómo funciona
Un circuito se mueve entre tres estados:
- Closed (cerrado) — las llamadas pasan. Cada falla consecutiva se cuenta;
una vez que alcanza
failure_threshold, el circuito se abre (trips open). - Open (abierto) — las llamadas se cortan de inmediato (fallback o
CircuitOpenException) sin tocar la dependencia. Trascooldown_seconds, la siguiente llamada se permite a modo de prueba: half-open (semiabierto). - Half-open (semiabierto) — se dejan pasar llamadas de prueba.
success_thresholdéxitos seguidos cierran el circuito; una sola falla lo vuelve a abrir.
Uso
Con un fallback
Cuando se proporciona un fallback, un circuito abierto (o una llamada que falla)
lo devuelve en lugar de lanzar una excepción. El fallback recibe el
Throwable:
$rate = CircuitBreaker::for('fx-api')->call( fn () => $this->fetchLiveRate(), fallback: fn (\Throwable $e) => $this->lastKnownRate(), );
Sin un fallback
Omítelo y el breaker relanza la excepción subyacente —o lanza
CircuitOpenException mientras está abierto— para que la manejes tú mismo:
use Webrek\CircuitBreaker\Exceptions\CircuitOpenException; try { CircuitBreaker::for('payments')->call(fn () => $gateway->charge($order)); } catch (CircuitOpenException $e) { return back()->withErrors('Payments are temporarily unavailable.'); }
No toda excepción es una falla
Un 422 por un error de validación significa que tu solicitud estaba mal, no
que el servicio esté caído: no debería abrir el breaker. Enumera esas
excepciones bajo ignore y pasarán de largo sin afectar al circuito:
// config/circuit-breaker.php 'defaults' => [ 'ignore' => [ Illuminate\Http\Client\RequestException::class, // solo si tratas los 4xx como error del llamador ], ],
Se combina con el outbox
El relay de webrek/laravel-outbox puede entregar a través de un breaker para que deje de reintentar contra un endpoint que ya está caído, y reanude automáticamente una vez que se recupera.
Observabilidad y operación
Los eventos del ciclo de vida te permiten alertar ante cambios de estado:
| Evento | Cuándo |
|---|---|
CircuitOpened |
Un circuito se abrió. |
CircuitHalfOpened |
Un circuito abierto inició una prueba de recuperación. |
CircuitClosed |
Un circuito se recuperó. |
Fuerza el cierre de un circuito a mano:
php artisan circuit-breaker:reset payments
Inspecciona el estado en código con CircuitBreaker::for('payments')->state() y
->available().
Configuración
return [ 'cache' => [ 'store' => env('CIRCUIT_BREAKER_CACHE'), // null = por defecto; usa Redis en producción 'prefix' => 'circuit-breaker', 'ttl' => 86400, ], 'defaults' => [ 'failure_threshold' => 5, // fallos consecutivos que abren el circuito 'cooldown_seconds' => 30, // open → half-open después de esto 'success_threshold' => 1, // éxitos de prueba necesarios para cerrar 'ignore' => [], // excepciones que no cuentan como fallos ], 'circuits' => [ 'payments' => ['failure_threshold' => 3, 'cooldown_seconds' => 60], ], ];
Requisitos
| Componente | Versión |
|---|---|
| PHP | 8.2+ |
| Laravel | 12.x / 13.x |
| Cache | Un almacén compartido (Redis) en producción |
Pruebas
composer install
composer test
Contribuir
Consulta CONTRIBUTING.md.
Seguridad
Por favor revisa la política de seguridad antes de reportar una vulnerabilidad.
Licencia
Publicado bajo la licencia MIT.