Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Circuit Breaker Laravel Package

algoyounes/circuit-breaker

A Laravel circuit breaker for protecting your app from failing external services. It prevents cascading failures by tripping after repeated errors, supports configurable thresholds and fallbacks, and helps keep requests resilient during outages.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require algoyounes/circuit-breaker
php artisan vendor:publish --tag=circuit-breaker-config

Start by reading config/circuit-breaker.php — it is the whole mental model of the package. You are looking for three knobs per breaker: failure threshold (how many failures before the circuit opens), open/cooldown timeout (how long it stays open before allowing a trial request), and the cache store used to persist state. Everything else is ergonomics around those three.

The first use case to wire up is always the same: an outbound call to a third-party you do not control (payment gateway, SMS provider, geo-IP lookup, internal microservice). Wrap it, give it a named breaker, and give it a fallback:

use Algoyounes\CircuitBreaker\Facades\CircuitBreaker;

$rates = CircuitBreaker::for('currency-api')
    ->fallback(fn () => Cache::get('rates.fallback'))
    ->run(fn () => Http::timeout(3)->get('https://rates.example.com/latest')->json());

Verify it works by pointing the breaker at a URL that 500s, hitting it threshold times, and watching the next call short-circuit instead of waiting on the timeout. Do that in php artisan tinker before writing any application code — it takes two minutes and tells you whether your cache store is actually shared (see Gotchas).

On v1.8.1: this release is purely Laravel 13 support. Nothing to migrate, but it is a signal that the package is being maintained against current framework versions — worth confirming your composer.json PHP constraint (Laravel 13 requires PHP 8.3+) and, if you have a CI matrix, adding 13.x alongside your existing laravel/framework entries. If you are still on Laravel 10 or 11, pin to the release line that matches and do not blindly composer update across the major.

Implementation Patterns

One breaker per dependency, named after the dependency. Not per controller, not per model. stripe, twilio, geoip, inventory-service. The breaker's state is only meaningful if all traffic to that dependency shares it; a breaker per-endpoint fragments your failure signal and lets you open five circuits for one sick service.

Wrap at the boundary, not in the controller. Put the breaker inside the service class or client wrapper that owns the integration, so every caller inherits the protection and the breaker key is defined in exactly one place. A controller-level CircuitBreaker::for(...) call is a smell — it means some other code path is calling the same API unprotected.

Separate the "circuit is open" path from the "call failed" path. You generally want two behaviours: a fast fallback when the circuit is open (serve stale cache, queue for later, return a degraded response), and normal error handling when the call actually failed this time. Structure it explicitly rather than relying on one generic catch.

Compose with, don't replace, Laravel's HTTP retry. Http::retry(2, 100) handles transient blips; the breaker handles sustained outage. Retrying inside a breaker means one logical call can burn three failure slots — which may be what you want, but decide it deliberately. A common sane config: retry 1–2 times with short backoff, breaker threshold set to roughly threshold / retries of your real tolerance.

Degrade gracefully, not silently. Fallbacks that return null or an empty array push the failure downstream into confusing TypeErrors. Return a well-shaped object, a cached value, or throw a domain-specific exception the caller can handle. Never let a fallback call the same failing dependency.

Wrap queue jobs too. A breaker that only guards synchronous web requests still lets 10,000 queued jobs hammer a dead API. Use the same breaker key from the job — the state is shared, so web and queue traffic trip the circuit together. Combine with $job->release() or a dead-letter path for when the circuit is open at dispatch time.

Instrument state transitions. Log or emit an event when a breaker opens and when it closes. A circuit opening is a production incident signal; a circuit that has been open for an hour and nobody noticed is worse than the original outage. If the package exposes events, hook them; otherwise wrap your breaker call in a small helper that logs on the exception path.

Test the open state explicitly. Use Cache::flush() / a test-specific cache store between tests so breakers don't leak state across test cases — this is the single most common source of flaky tests in codebases using this pattern. Assert both branches: N failures then a fast short-circuit, and the half-open recovery after the cooldown (freeze time or set the cooldown to 1 second in test config).

Gotchas and Tips

Cache store is the whole ballgame. Breaker state lives in Laravel's cache. On a multi-server deployment, file or array gives every node its own private circuit — you will "open" the breaker and watch 90% of traffic sail straight through. Use Redis or a shared database store, and pin it explicitly in config rather than inheriting CACHE_STORE, which developers routinely flip to array locally and accidentally in staging.

array store in tests also means breakers don't persist across requests. Convenient for isolation, dangerous if you're using a feature test to prove a breaker opens — you need to make the failing calls within a single test method, or explicitly configure a persistent store for that test.

Failures must be counted on the exceptions you think they are. A breaker that counts Exception will trip on a ValidationException or a ModelNotFoundException caused by bad input, opening the circuit for everyone because one user sent garbage. Catch narrowly, or explicitly whitelist which exception types increment the failure count.

Half-open is not a thundering herd. When the cooldown elapses, most implementations let the next request through as a trial. Under concurrency, several requests can slip through simultaneously before the first one reports back. If the downstream is fragile, add a small delay or gate the trial with a lock; if it just needs to recover, this is usually fine — but know which you have.

Timeouts are the actual protection. The circuit breaker stops you calling a dead service; it does not stop a slow-but-alive one. Always set an explicit Http::timeout() (and connectTimeout()) inside the breaker. Without it, a hung connection holds workers for the full PHP max_execution_time and you get an outage that the breaker never registers.

Don't wrap database transactions or anything inside one. Tripping a breaker mid-transaction leaves you with an open transaction and a rollback you didn't plan. Do the breaker-guarded call before opening the transaction, or after committing.

Tune per dependency, not globally. A payment gateway deserves a low threshold and a long cooldown (you would rather degrade checkout briefly than send half-charged orders). A non-critical enrichment API can have a high threshold and short cooldown so it recovers quickly. Hardcoding one global threshold is the most common misconfiguration in first deployments.

Extension points to reach for: the config file for per-breaker overrides, the fallback callback for degradation, and any published interface/contract the package exposes for swapping the storage or state strategy. If you find yourself subclassing internals, check whether the package offers a binding or config switch first — internals are the part most likely to move between minor releases.

Version pinning around 1.8.x. Since 1.8.1 is a framework-compatibility release with no API changes, ^1.8 is safe and will pick up further Laravel support without surprises. Watch the changelog for anything that touches config/circuit-breaker.php keys — config key renames in packages like this usually land as minor versions with a silent default, which is exactly the class of change that quietly disables your breaker in production. After any upgrade, re-run your "trip the breaker on purpose" check.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib