gabrielanhaia / laravel-circuit-breaker
Laravel integration for PHP Circuit Breaker — multiple storage drivers, middleware, Artisan commands, and event system
Installs: 1 006
Dependents: 0
Suggesters: 0
Security: 0
Stars: 44
Watchers: 1
Forks: 3
Open Issues: 0
pkg:composer/gabrielanhaia/laravel-circuit-breaker
Requires
- php: ^8.2
- gabrielanhaia/php-circuit-breaker: ^3.0
- illuminate/contracts: ^11.0||^12.0
- illuminate/support: ^11.0||^12.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.40
- larastan/larastan: ^2.9||^3.0
- mockery/mockery: ^1.5
- orchestra/testbench: ^9.0||^10.0
- phpstan/phpstan: ^1.10||^2.0
- phpunit/phpunit: ^11.5||^12.0
Suggests
- ext-apcu: Required for the APCu storage driver
- ext-memcached: Required for the Memcached storage driver
- ext-redis: Required for the Redis storage driver (phpredis)
README
A Laravel integration for PHP Circuit Breaker v3 with multiple storage drivers, HTTP middleware, Artisan commands, and full event system integration.
What Is a Circuit Breaker?
When your application calls an external service (payment gateway, email API, etc.), that service can become slow or unresponsive. Without protection, your application keeps sending requests — piling up timeouts, wasting resources, and degrading user experience.
A circuit breaker monitors failures and automatically stops calling a failing service, giving it time to recover. It works like an electrical circuit breaker: when too many failures occur, the circuit "opens" and all requests fail fast without actually hitting the remote service.
stateDiagram-v2
[*] --> Closed
Closed --> Open : Failures reach threshold
Open --> HalfOpen : Timeout expires
HalfOpen --> Closed : Success threshold met
HalfOpen --> Open : Any failure
Loading
| State | Behaviour |
|---|---|
| Closed | Normal operation. Requests pass through. Failures are counted. |
| Open | Requests are blocked immediately (fail fast). No calls reach the service. |
| Half-Open | A limited number of probe requests are allowed through to test recovery. |
Requirements
- PHP ^8.2
- Laravel ^11.0 or ^12.0
- gabrielanhaia/php-circuit-breaker ^3.0 (installed automatically)
Installation
composer require gabrielanhaia/laravel-circuit-breaker
Publish the configuration file:
php artisan vendor:publish --tag=circuit-breaker-config
Configuration
After publishing, the config lives at config/circuit_breaker.php.
Storage Driver
Choose where circuit state is persisted:
<?php // config/circuit_breaker.php return [ 'default_driver' => env('CIRCUIT_BREAKER_DRIVER', 'redis'), 'drivers' => [ 'redis' => [ 'connection' => env('CIRCUIT_BREAKER_REDIS_CONNECTION', 'default'), 'prefix' => 'cb:', ], 'apcu' => [ 'prefix' => 'cb:', ], 'memcached' => [ 'connection' => env('CIRCUIT_BREAKER_MEMCACHED_CONNECTION', 'memcached'), 'prefix' => 'cb:', ], 'array' => [], ], // ... ];
| Driver | Best for | Requirement |
|---|---|---|
redis |
Distributed / multi-server apps | ext-redis (phpredis) |
apcu |
Single-server, high performance | ext-apcu |
memcached |
Distributed caching | ext-memcached |
array |
Testing and local development | none |
Note: The Redis driver requires the phpredis PHP extension. Predis is not supported by the underlying storage adapter.
Default Thresholds
<?php // config/circuit_breaker.php return [ // ... 'defaults' => [ 'failure_threshold' => 5, // Number of failures to open the circuit 'success_threshold' => 1, // Successes in half-open to close the circuit 'time_window' => 20, // Seconds in which failures are counted 'open_timeout' => 30, // Seconds the circuit stays open before half-open 'half_open_timeout' => 20, // Seconds the circuit stays half-open 'exceptions_enabled' => false, // true = throw OpenCircuitException instead of returning false ], // ... ];
Per-Service Overrides
Different services can have different thresholds. Any key you omit falls back to the value in defaults:
<?php // config/circuit_breaker.php return [ // ... 'services' => [ 'payment-api' => [ 'failure_threshold' => 3, // More sensitive — fewer failures to trip 'open_timeout' => 60, // Longer cooldown ], 'email-service' => [ 'failure_threshold' => 10, // More tolerant ], ], ];
Usage
Facade
<?php namespace App\Services; use GabrielAnhaia\LaravelCircuitBreaker\Facades\CircuitBreaker; use Illuminate\Support\Facades\Http; class PaymentService { public function charge(array $data): mixed { if (!CircuitBreaker::canPass('payment-api')) { return $this->fallback(); } try { $response = Http::timeout(5)->post('https://api.payment.example/charge', $data); CircuitBreaker::recordSuccess('payment-api'); return $response->json(); } catch (\Throwable $e) { CircuitBreaker::recordFailure('payment-api'); return $this->fallback(); } } private function fallback(): array { return ['status' => 'queued', 'message' => 'Payment will be retried shortly.']; } }
Dependency Injection
<?php namespace App\Services; use GabrielAnhaia\LaravelCircuitBreaker\CircuitBreakerManager; use Illuminate\Support\Facades\Http; class PaymentService { public function __construct( private readonly CircuitBreakerManager $circuitBreaker, ) {} public function charge(array $data): mixed { if (!$this->circuitBreaker->canPass('payment-api')) { return $this->fallback(); } try { $response = Http::timeout(5)->post('https://api.payment.example/charge', $data); $this->circuitBreaker->recordSuccess('payment-api'); return $response->json(); } catch (\Throwable $e) { $this->circuitBreaker->recordFailure('payment-api'); return $this->fallback(); } } private function fallback(): array { return ['status' => 'queued']; } }
HTTP Middleware
The circuit-breaker middleware wraps an entire route. It blocks requests when the circuit is open and automatically records successes and failures based on the HTTP status code.
flowchart LR
A[Incoming Request] --> B{Circuit open?}
B -- Yes --> C[503 Service Unavailable]
B -- No --> D[Execute route]
D --> E{Response 5xx?}
E -- Yes --> F[recordFailure]
E -- No --> G[recordSuccess]
F --> H[Return response]
G --> H
Loading
Register the middleware on your routes:
<?php // routes/api.php use App\Http\Controllers\PaymentController; use Illuminate\Support\Facades\Route; Route::middleware('circuit-breaker:payment-api') ->post('/payments/charge', [PaymentController::class, 'charge']); Route::middleware('circuit-breaker:email-service') ->post('/notifications/send', [NotificationController::class, 'send']);
When the circuit is open the middleware returns a JSON response:
{
"message": "Service unavailable."
}
with HTTP status 503 Service Unavailable.
Artisan Commands
# Check the current state of a service's circuit php artisan circuit-breaker:status payment-api # Force a state override (useful during deploys or incidents) php artisan circuit-breaker:force payment-api open php artisan circuit-breaker:force payment-api closed --ttl=300 # auto-expires in 5 min # Remove a manual override and return to normal state logic php artisan circuit-breaker:clear payment-api
Events
All circuit breaker events are dispatched through Laravel's event system, so you can listen to them like any other Laravel event:
<?php // app/Providers/AppServiceProvider.php namespace App\Providers; use GabrielAnhaia\PhpCircuitBreaker\Event\CircuitOpenedEvent; use GabrielAnhaia\PhpCircuitBreaker\Event\CircuitClosedEvent; use GabrielAnhaia\PhpCircuitBreaker\Event\FailureRecordedEvent; use GabrielAnhaia\PhpCircuitBreaker\Event\SuccessRecordedEvent; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Event::listen(CircuitOpenedEvent::class, function (CircuitOpenedEvent $event): void { Log::warning("Circuit OPENED for [{$event->getServiceName()}]"); }); Event::listen(CircuitClosedEvent::class, function (CircuitClosedEvent $event): void { Log::info("Circuit CLOSED for [{$event->getServiceName()}]"); }); } }
| Event | Fired when |
|---|---|
FailureRecordedEvent |
A failure is recorded |
SuccessRecordedEvent |
A success is recorded |
CircuitOpenedEvent |
The circuit transitions to OPEN |
CircuitClosedEvent |
The circuit transitions to CLOSED |
CircuitHalfOpenEvent |
The circuit transitions to HALF_OPEN |
Every event exposes getServiceName(): string and getOccurredAt(): DateTimeImmutable.
Manual Override
Force a circuit state for maintenance windows or during an incident:
<?php use GabrielAnhaia\LaravelCircuitBreaker\Facades\CircuitBreaker; use GabrielAnhaia\PhpCircuitBreaker\CircuitState; // Block all traffic to a service (force OPEN) CircuitBreaker::forceState('payment-api', CircuitState::OPEN); // Block for 5 minutes, then automatically return to normal state logic CircuitBreaker::forceState('payment-api', CircuitState::OPEN, ttl: 300); // Remove the override CircuitBreaker::clearOverride('payment-api');
Upgrade from v1
See UPGRADE-2.0.md for step-by-step migration instructions.
Development
composer test # Run PHPUnit (unit + feature) composer phpstan # Static analysis — level max composer cs-check # Code style dry-run composer cs-fix # Auto-fix code style
Support
If you find this package useful, consider supporting it:
License
MIT. See LICENSE for details.
