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 Php Laravel Package

leocarmo/circuit-breaker-php

A circuit breaker implementation for PHP that protects applications from cascading failures by monitoring failing dependencies and temporarily blocking calls until they recover. Configure thresholds, timeouts, and storage adapters to build resilient service integrations.

Deep Wiki
Context7

Getting Started

Install with Composer:

composer require leocarmo/circuit-breaker-php

Requirements depend on the adapter you pick — the core package is storage-agnostic, but the shipped adapters target specific clients (ext-redis / Predis, ext-memcached, ext-mongodb). If you're unsure, start with Redis: it's the adapter most teams deploy, and it works across multiple PHP workers so failure counts aren't per-process.

Where to look first

  • README.md — adapter list and the exact setter names (setCircuit, setTimeWindow, setFailureThreshold, setTimeout).
  • src/CircuitBreaker.php — the entire public surface (isAvailable, success, failure, getState). It's a small class; read it once and you'll know the whole package.
  • src/Adapters/ — RedisAdapter, PredisAdapter, MemcachedAdapter, MongoDBAdapter. Each implements the same storage contract, so the circuit logic is identical.

Minimal first use case — protect one flaky outbound call:

use LeoCarmo\CircuitBreaker\CircuitBreaker;
use LeoCarmo\CircuitBreaker\Adapters\RedisAdapter;

$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);

$breaker = new CircuitBreaker(new RedisAdapter($redis));

$breaker->setCircuit('billing-api')
        ->setTimeWindow(60)        // seconds to aggregate failures
        ->setFailureThreshold(5)   // failures within window to open
        ->setTimeout(30);          // seconds before a half-open probe

if ($breaker->isAvailable()) {
    try {
        $response = $http->post('https://billing.internal/charge', $payload);
        $breaker->success();
    } catch (\Throwable $e) {
        $breaker->failure();
        throw $e;
    }
} else {
    // fast fallback: queue the charge, return cached result, etc.
}

That's the whole loop: check availability → call → report success()/failure() → fall back when open.

Implementation Patterns

Bind it once, configure it per circuit. In a Laravel app, put the CircuitBreaker (and its adapter) in a singleton binding, then wrap configuration in a small helper that sets the circuit name and thresholds before every call. Don't rely on the previous caller's config — the setters mutate the instance.

// app/Providers/AppServiceProvider.php
$this->app->singleton(CircuitBreaker::class, function () {
    $redis = new \Redis();
    $redis->connect(config('database.redis.default.host'), config('database.redis.default.port'));
    return new CircuitBreaker(new RedisAdapter($redis));
});
// app/Support/Circuit.php
final class Circuit
{
    public function __construct(private CircuitBreaker $breaker) {}

    public function run(string $name, array $cfg, Closure $call, Closure $fallback)
    {
        $this->breaker
            ->setCircuit($name)
            ->setTimeWindow($cfg['window'])
            ->setFailureThreshold($cfg['threshold'])
            ->setTimeout($cfg['timeout']);

        if (! $this->breaker->isAvailable()) {
            return $fallback();
        }

        try {
            $result = $call();
            $this->breaker->success();
            return $result;
        } catch (\Throwable $e) {
            $this->breaker->failure();
            throw $e;
        }
    }
}

Wrap the dependency, not the controller. Circuit breakers belong in the gateway/repository class that talks to a remote service. Keeping the breaker at the HTTP client layer means retries, retries with backoff, and multiple endpoints all share one circuit — which is usually what you want, since a degraded downstream fails on all of them anyway.

One circuit per downstream service. Name circuits after the dependency (billing-api, inventory, payments-stripe) rather than per endpoint. Fine-grained circuits blow past thresholds only under specific traffic mixes and rarely open when you need them.

Pair with timeout + retry, not instead of. The breaker prevents hammering a dead dependency; retries smooth over transient blips. Order matters: retry the individual call inside the breaker's isAvailable() check, and report a single failure() when the retry budget is exhausted. A retry loop outside the breaker defeats the whole purpose.

Use it to fail fast, then degrade gracefully. The else branch is the real product surface — return a cached value, queue the operation, or return a default. If your fallback just re-throws the exception, the breaker hasn't bought you anything except slower timeouts.

Half-open is your probe channel. When setTimeout expires, the next isAvailable() flips to half-open and lets one request through. If it succeeds the circuit closes; if it fails it reopens. Keep the first post-open request cheap (health check, HEAD, read-only lookup) so a still-broken service doesn't take another hit.

Laravel HTTP client middleware. For outbound calls that go through Http::, wrap the breaker in a middleware so every request goes through the same logic:

Http::withMiddleware(new CircuitBreakerMiddleware($breaker))->post(...);

The middleware checks availability before the request, reports success()/failure() after, and throws a CircuitOpenException (custom) so callers can catch it distinctly from a network error.

Test against a fake adapter. For feature tests, swap the storage adapter for a stub that records failures and returns isAvailable() based on a configurable flag. This lets you exercise the open path without flapping real circuits in shared Redis. If you point tests at a real Redis, use a distinct prefix per test run (see gotchas).

Log state transitions, not just calls. Emitting a structured log when the state transitions CLOSED → OPEN or OPEN → HALF_OPEN turns the breaker into a monitoring signal — you'll see which dependencies are degrading before pager alerts fire.

Gotchas and Tips

  • Setters are instance state, not circuit state. setCircuit(), setTimeout(), etc. mutate the breaker object. In a singleton (or Octane worker) that means the last configured circuit wins for any code that doesn't re-set it. Always configure before use, or make a Circuit::for($name) factory that returns a fresh, configured instance.

  • Redis key collisions across environments. The Redis adapter uses the circuit name in its keys. If staging and prod share a Redis, or two services use a generic name like api, they'll share counters. Prefix circuit names (prod:billing-api) or use a separate Redis database.

  • Fail-open vs. fail-closed is your choice — the package won't decide for you. If Redis is unreachable, the adapter throws on isAvailable(). That's fail-closed at the breaker level, which usually surfaces as a 500 in the caller. Wrap the breaker call in a try/catch and decide explicitly: for critical flows, log and proceed (fail-open); for optional calls, skip.

  • Half-open has no locking. The package doesn't coordinate which concurrent request gets the probe. Under load, several in-flight requests can pass through half-open at once and stampede a recovering service. If that matters, layer your own mutex (Redis SET NX with a short TTL) around the probe.

  • success() in an OPEN state can close the circuit prematurely. Only call success() on requests that were actually let through by isAvailable(). Background warmup jobs, health checks, and cache refreshes that call success() will quietly heal circuits that should stay open.

  • Count the right failures. A 422 from a validation error is not a downstream outage; a 503 is. Classify before calling failure() — otherwise routine user errors will trip the breaker. Conversely, connection timeouts, 5xx, and 429s belong in the failure bucket.

  • Tune thresholds from data, not defaults. The default setFailureThreshold is low. In a 50-rps service, 5 failures is a blip; in a 0.5-rps service, it's real. Set the window wide enough to smooth traffic noise (60–120s is typical) and the timeout long enough that a restarting service can come up (30–60s).

  • Clock skew matters less than you think, but still matters. The time window is evaluated against stored timestamps. If Redis and your app servers disagree by minutes, thresholds behave erratically. Keep NTP healthy and prefer a single shared adapter over per-server in-memory state.

  • Octane / long-running workers. A singleton CircuitBreaker holds a persistent Redis connection. If that connection drops, the adapter throws on every subsequent call. Bind it per-request, or wrap the adapter's connection acquisition in a retry with a short timeout — and never let a broken breaker take down the whole request path.

  • Testing is easy — mock the storage interface. The StateStorage contract is small; a hand-rolled in-memory fake (isAvailable, success, failure, getState) is trivial and much faster than a real Redis, and it lets you assert that failure() was called for the right exceptions.

  • Extension points are the storage layer. If your team uses DynamoDB or Postgres, implement the storage interface — nothing else changes. That's the right place to add TTL cleanup, metrics emission, or per-tenant keying. Don't fork CircuitBreaker itself; it's small enough to leave alone.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core