PHPackages                             harris21/laravel-fuse - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. harris21/laravel-fuse

ActiveLibrary[Queues &amp; Workers](/categories/queues)

harris21/laravel-fuse
=====================

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

v0.8.0(1mo ago)46273.9k↑258.1%18[1 PRs](https://github.com/harris21/laravel-fuse/pulls)MITPHPPHP ^8.3CI passing

Since Feb 2Pushed 1mo ago2 watchersCompare

[ Source](https://github.com/harris21/laravel-fuse)[ Packagist](https://packagist.org/packages/harris21/laravel-fuse)[ RSS](/packages/harris21-laravel-fuse/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (10)Dependencies (44)Versions (20)Used By (0)

 [![Fuse for Laravel](art/logo.png)](art/logo.png)

 **Circuit breaker for Laravel queue jobs**

 Protect your queue workers from cascading failures when external services go down.

---

The Problem
-----------

[](#the-problem)

When Stripe goes down at 11 PM, your queue workers don't know. They keep trying to charge customers. Each job waits 30 seconds for a timeout. Then retries. Waits again. Your entire queue system freezes.

**Without Fuse:** 10,000 jobs × 30-second timeouts = 25+ hours to clear the queue.

**With Fuse:** Circuit opens after 5 failures. Queue clears in 10 seconds. Automatic recovery when the service returns.

---

Features
--------

[](#features)

- **Three-State Circuit Breaker** — CLOSED (normal), OPEN (protected), HALF-OPEN (testing recovery)
- **Intelligent Failure Classification** — 429 rate limits and auth errors don't trip the circuit
- **Peak Hours Support** — Different thresholds for business hours vs. off-peak
- **Configurable Window Tracking** — Tumbling time buckets (default 60s, tunable per service) with automatic expiration, no cleanup needed
- **Thundering Herd Prevention** — `Cache::lock()` ensures only one worker probes during recovery
- **Zero Data Loss** — Jobs are delayed with `release()`, not failed permanently
- **Automatic Recovery** — Circuit tests and heals itself when services return
- **Optional Recovery Strategies** — Single-probe works out of the box; plug in your own strategy if a service needs to warm back up differently
- **Per-Service Circuits** — Separate breakers for Stripe, Mailgun, your microservices
- **Laravel Events** — Get notified on state transitions for alerting and monitoring
- **Real-Time Status Page** — Built-in monitoring dashboard with live state updates
- **Pure Laravel** — No external dependencies, uses Cache and native job middleware

---

How It Works
------------

[](#how-it-works)

 [![Circuit Breaker States](art/circuit-states.png)](art/circuit-states.png)

**CLOSED** — Normal operations. All requests pass through. Failures are tracked in the background.

**OPEN** — Protection mode. After the failure threshold is exceeded, the circuit trips. Jobs fail instantly (1ms, not 30s) and are delayed for automatic retry. No API calls are made.

**HALF-OPEN** — Testing recovery. After the timeout period, one probe request tests if the service recovered. Success closes the circuit. Failure reopens it. If you need to, you may [customize the probe request](#recovery-strategies).

---

Installation
------------

[](#installation)

```
composer require harris21/laravel-fuse
```

Publish the configuration:

```
php artisan vendor:publish --tag=fuse-config
```

---

Quick Start
-----------

[](#quick-start)

Add the middleware to your job:

```
use Harris21\Fuse\Middleware\CircuitBreakerMiddleware;

class ChargeCustomer implements ShouldQueue
{
    public $tries = 0;           // Unlimited releases
    public $maxExceptions = 3;   // Only real failures count

    public function middleware(): array
    {
        return [new CircuitBreakerMiddleware('stripe', release: 20)];
    }

    public function handle(): void
    {
        // Your payment logic - unchanged
        Stripe::charges()->create([...]);
    }
}
```

That's it. Your job is now protected.

---

Attributes
----------

[](#attributes)

Instead of building the middleware array yourself, declare protection with attributes:

```
use Harris21\Fuse\Attributes\UseCircuitBreaker;
use Harris21\Fuse\Middleware\ResolvesCircuitBreakers;

#[UseCircuitBreaker('stripe')]
class ChargeCustomer implements ShouldQueue
{
    public function middleware(): array
    {
        return ResolvesCircuitBreakers::resolve($this);
    }
}
```

Jobs that talk to multiple services can stack them:

```
#[UseCircuitBreaker('stripe')]
#[UseCircuitBreaker('mailgun', release: 30)]
class ChargeAndNotify implements ShouldQueue
{
    public function middleware(): array
    {
        return ResolvesCircuitBreakers::resolve($this);
    }
}
```

Both `release:` and `window:` can be set per attribute. `window:` overrides the failure-tracking window (in seconds) for that service, taking precedence over config:

```
#[UseCircuitBreaker(service: 'reports', window: 600)]
```

If you have other middleware to include, use `merge()` to prepend the circuit breakers:

```
public function middleware(): array
{
    return ResolvesCircuitBreakers::merge($this, [
        new RateLimited('payments'),
    ]);
}
```

---

Configuration
-------------

[](#configuration)

```
// config/fuse.php

return [
    'enabled' => env('FUSE_ENABLED', true),

    'default_threshold' => 50,      // Failure rate percentage to trip circuit
    'default_timeout' => 60,        // Seconds before testing recovery
    'default_min_requests' => 10,   // Minimum requests before evaluating
    'default_window' => 60,         // Seconds per failure-tracking window

    'services' => [
        'stripe' => [
            'threshold' => 50,
            'timeout' => 30,
            'min_requests' => 5,
            'release' => 15,

            // Peak hours: more tolerant during business hours
            'peak_hours_threshold' => 60,
            'peak_hours_start' => 9,   // 9 AM
            'peak_hours_end' => 17,    // 5 PM
        ],
        'mailgun' => [
            'threshold' => 60,
            'timeout' => 120,
            'min_requests' => 10,
            'window' => 300,        // 5-minute window for this lower-traffic service
        ],
    ],

    // Cache prefix — change if multiple apps share the same Redis instance
    'cache' => [
        'prefix' => env('FUSE_CACHE_PREFIX', 'fuse'),
    ],
];
```

---

Peak Hours
----------

[](#peak-hours)

Configure different thresholds for business hours when every transaction matters:

```
'stripe' => [
    'threshold' => 40,              // Off-peak: more sensitive (40%)
    'peak_hours_threshold' => 60,   // Peak hours: more tolerant (60%)
    'peak_hours_start' => 9,        // 9 AM
    'peak_hours_end' => 17,         // 5 PM
],
```

During peak hours (9 AM - 5 PM), the circuit uses the higher threshold to maximize successful transactions. Outside peak hours, it uses the lower threshold for earlier protection.

---

Tracking Window
---------------

[](#tracking-window)

Failures are counted in fixed time windows (tumbling buckets), and the circuit only evaluates the failure rate once a window has gathered `min_requests` attempts. This matters for low-volume services: if a service doesn't see `min_requests` attempts within a single window, the failure rate is never checked and the circuit can't trip. The default 60-second window fits busy services — quieter ones need a wider window so enough attempts accumulate.

Widen the window so enough samples accumulate before the bucket rolls over:

```
'reports' => [
    'min_requests' => 10,
    'window' => 600,   // 10 minutes — long enough to gather 10 samples
],
```

Set it globally with `default_window`, per-service as above, or inline on the attribute (`#[UseCircuitBreaker(service: 'reports', window: 600)]`). Counters auto-expire after twice the window, so there's still nothing to clean up.

Trade-offs to keep in mind: a longer window reacts more slowly and keeps the circuit eligible to stay open longer, and because buckets are tumbling, a failure burst that straddles a boundary is split across two windows — so worst-case detection can lag by up to one window. If you ever need smoother behaviour, a future weighted "current + previous bucket" counter could remove that boundary effect.

---

Intelligent Failure Classification
----------------------------------

[](#intelligent-failure-classification)

Not all errors indicate a service is down. Fuse only counts real outages:

Error TypeCounted as Failure?Reason500, 502, 503YesServer errors indicate service problemsConnection timeoutYesService is unreachableConnection refusedYesService is unreachable429 Too Many RequestsNoService is healthy, just rate limiting401 UnauthorizedNoYour API key is wrong, not a service issue403 ForbiddenNoPermission issue, not a service outage400 Bad RequestYesCould indicate API issues404 Not FoundYesCould indicate API changesThis prevents false positives. A rate limit doesn't mean Stripe is down - it means you're sending too many requests.

---

Custom Failure Classification
-----------------------------

[](#custom-failure-classification)

The default behavior works well for most APIs, but some services deviate from HTTP standards. For example, Stripe returns `500` for idempotency errors that are actually client-side issues — not outages.

You can override the failure classification logic per service by setting the `failure_classifier` option in your service config:

```
// config/fuse.php

'services' => [
    'stripe' => [
        'threshold' => 50,
        'timeout' => 30,
        'min_requests' => 5,
        'failure_classifier' => \App\Fuse\StripeFailureClassifier::class,
    ],
],
```

### Extending the Default Classifier

[](#extending-the-default-classifier)

The easiest approach is to extend `DefaultFailureClassifier` and override specific cases:

```
namespace App\Fuse;

use GuzzleHttp\Exception\ServerException;
use Harris21\Fuse\Classifiers\DefaultFailureClassifier;
use Throwable;

class StripeFailureClassifier extends DefaultFailureClassifier
{
    public function shouldCount(Throwable $e): bool
    {
        // Stripe returns 500 for idempotency errors — not a real outage
        if ($e instanceof ServerException) {
            $body = (string) $e->getResponse()?->getBody();

            if (str_contains($body, 'idempotency')) {
                return false;
            }
        }

        return parent::shouldCount($e);
    }
}
```

### Implementing the Interface from Scratch

[](#implementing-the-interface-from-scratch)

For full control, implement `FailureClassifier` directly:

```
namespace App\Fuse;

use Harris21\Fuse\Contracts\FailureClassifier;
use Throwable;

class CustomFailureClassifier implements FailureClassifier
{
    public function shouldCount(Throwable $e): bool
    {
        // Your classification logic
    }
}
```

When no `failure_classifier` is configured, Fuse uses `DefaultFailureClassifier` which preserves the behavior described in the table above.

---

Recovery strategies
-------------------

[](#recovery-strategies)

By default, when a circuit is half-open, Fuse lets a single probe request through to test whether the service has recovered. If it succeeds, the circuit closes and traffic returns to full speed; if it fails, the circuit reopens. You don't need to configure anything for this — it's how Fuse works out of the box.

If you would like more control over how a recovered service is tested, you may provide your own recovery strategy by implementing the `RecoveryStrategy` contract. This is entirely optional.

For example, you might warm a slow service back up gradually instead of returning to full speed on the first success:

```
namespace App\Fuse;

use Harris21\Fuse\CircuitBreaker;
use Harris21\Fuse\Contracts\RecoveryStrategy;

class CustomRecoveryStrategy implements RecoveryStrategy
{
    public function allowsAttempt(CircuitBreaker $breaker): bool
    {
        // Return true to let this half-open job through, false to release it for a later retry.
    }

    public function recordSuccess(CircuitBreaker $breaker): bool
    {
        // Return true to fully close the circuit, false to keep testing.
    }

    public function recordFailure(CircuitBreaker $breaker): void
    {
        // Called when a half-open job fails. The circuit reopens around this call.
    }
}
```

Then reference your class via the `recovery_strategy` option on your service configuration. It is resolved through the container, just like `failure_classifier`:

```
'services' => [
    'stripe' => [
        'recovery_strategy' => \App\Fuse\CustomRecoveryStrategy::class,
    ],
],
```

Use `$breaker->key('your-suffix')` to namespace any cache keys your strategy needs.

---

Events
------

[](#events)

Fuse dispatches Laravel events on every state transition:

```
use Harris21\Fuse\Events\CircuitBreakerOpened;
use Harris21\Fuse\Events\CircuitBreakerHalfOpen;
use Harris21\Fuse\Events\CircuitBreakerClosed;
```

### Listening to Events

[](#listening-to-events)

```
// app/Listeners/AlertOnCircuitOpen.php

class AlertOnCircuitOpen
{
    public function handle(CircuitBreakerOpened $event): void
    {
        Log::critical("Circuit breaker opened for {$event->service}", [
            'failure_rate' => $event->failureRate,
            'attempts' => $event->attempts,
            'failures' => $event->failures,
        ]);

        // Send Slack notification, page on-call, etc.
    }
}
```

### Event Properties

[](#event-properties)

**CircuitBreakerOpened:**

- `$service` — The service name (e.g., "stripe")
- `$failureRate` — Current failure percentage
- `$attempts` — Total requests in the window
- `$failures` — Failed requests in the window

**CircuitBreakerHalfOpen:**

- `$service` — The service name

**CircuitBreakerClosed:**

- `$service` — The service name

---

Status Page
-----------

[](#status-page)

Fuse includes a real-time monitoring dashboard that shows the state of all your circuit breakers.

 [![Fuse Status Page](art/status-page.png)](art/status-page.png)

### Enable the Status Page

[](#enable-the-status-page)

Add to your `.env`:

```
FUSE_STATUS_PAGE_ENABLED=true
```

The status page is available at `/fuse` (configurable via `FUSE_STATUS_PAGE_PREFIX`).

### Authorization

[](#authorization)

Access is controlled by a `viewFuse` gate. By default, only the `local` environment is allowed. Override it in your `AppServiceProvider`:

```
use Illuminate\Support\Facades\Gate;

Gate::define('viewFuse', function ($user = null) {
    return $user?->isAdmin();
});
```

### Configuration

[](#configuration-1)

```
// config/fuse.php

'status_page' => [
    'enabled' => env('FUSE_STATUS_PAGE_ENABLED', false),
    'prefix' => env('FUSE_STATUS_PAGE_PREFIX', 'fuse'),
    'middleware' => [],          // Custom middleware (replaces default)
    'polling_interval' => 2,    // Frontend refresh interval in seconds
],
```

### What It Shows

[](#what-it-shows)

- **Circuit state** for each configured service (CLOSED, OPEN, HALF-OPEN)
- **State history** with timestamped transitions
- **Live stats** — attempts, failures, failure rate per window
- **Recovery info** — when the circuit opened and when it will test recovery
- **Auto-refresh** — polls the backend every 2 seconds (configurable)

---

Artisan Commands
----------------

[](#artisan-commands)

Fuse includes CLI commands for inspecting and manually controlling circuit breakers.

### Check circuit status

[](#check-circuit-status)

```
php artisan fuse:status           # all services
php artisan fuse:status stripe    # single service
```

Outputs a table with the current state, failure rate, request counts, and threshold for each circuit.

### Reset a circuit

[](#reset-a-circuit)

```
php artisan fuse:reset            # all services
php artisan fuse:reset stripe     # single service
```

Resets the circuit to CLOSED state and clears all stats for the current window.

### Manually open a circuit

[](#manually-open-a-circuit)

```
php artisan fuse:open stripe
```

Forces the circuit OPEN immediately. Useful when you know a service is down and want to protect your queue before failures accumulate. The circuit will recover automatically after the configured `timeout`.

### Manually close a circuit

[](#manually-close-a-circuit)

```
php artisan fuse:close stripe
```

Forces the circuit CLOSED immediately. Useful when a service has recovered but the circuit hasn't timed out yet.

---

Fallback Strategies
-------------------

[](#fallback-strategies)

When the circuit opens, your application needs a plan. Here are common strategies:

**Return cached data** — Show last known prices, cached shipping rates, or stale product info. Slightly stale data beats an error page.

**Use a fallback service** — Switch to a backup payment provider, or show "payment pending" and queue it for later.

**Queue for later** — Fuse already does this with `release()`. For synchronous requests, dispatch a job to retry when the circuit closes.

**Graceful degradation** — Hide the feature entirely. Can't load recommendations? Don't show that section. The page still works.

---

Direct Usage
------------

[](#direct-usage)

Use the circuit breaker directly outside of jobs:

```
use Harris21\Fuse\CircuitBreaker;

$breaker = new CircuitBreaker('stripe');

if (!$breaker->isOpen()) {
    try {
        $result = Stripe::charges()->create([...]);
        $breaker->recordSuccess();
        return $result;
    } catch (Exception $e) {
        $breaker->recordFailure($e);
        throw $e;
    }
} else {
    // Circuit is open - use fallback
    return $this->fallbackResponse();
}
```

### Check Circuit State

[](#check-circuit-state)

```
$breaker = new CircuitBreaker('stripe');

$breaker->isClosed();    // Normal operations
$breaker->isOpen();      // Protected, failing fast
$breaker->isHalfOpen();  // Testing recovery

$breaker->getStats();    // Get full statistics
$breaker->reset();       // Manually reset to closed
```

---

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11+
- Redis recommended for production, file cache may have race conditions during recovery probing

---

Credits
-------

[](#credits)

Built by [Harris Raftopoulos](https://x.com/harrisrafto) for [Laracon India 2026](https://laracon.in).

YouTube: [@harrisrafto](https://youtube.com/@harrisrafto)

Video walkthrough: [Watch on YouTube](https://www.youtube.com/watch?v=w-QKqTPbcqs)

Based on the circuit breaker pattern from Michael Nygard's *Release It!* and popularized by Martin Fowler.

---

License
-------

[](#license)

MIT

###  Health Score

56

—

FairBetter than 97% of packages

Maintenance90

Actively maintained with recent releases

Popularity53

Moderate usage in the ecosystem

Community19

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 51.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~10 days

Recently: every ~20 days

Total

15

Last Release

46d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/0fcd0f4186fd1784c24293005e0c1468bb229f8a07f6b502b72fb8f034a284b9?d=identicon)[harris21](/maintainers/harris21)

---

Top Contributors

[![harris21](https://avatars.githubusercontent.com/u/1542015?v=4)](https://github.com/harris21 "harris21 (31 commits)")[![Button99](https://avatars.githubusercontent.com/u/56029580?v=4)](https://github.com/Button99 "Button99 (20 commits)")[![Copilot](https://avatars.githubusercontent.com/in/1143301?v=4)](https://github.com/Copilot "Copilot (4 commits)")[![geangontijo](https://avatars.githubusercontent.com/u/64979293?v=4)](https://github.com/geangontijo "geangontijo (2 commits)")[![ahinkle](https://avatars.githubusercontent.com/u/17038330?v=4)](https://github.com/ahinkle "ahinkle (1 commits)")[![lloricode](https://avatars.githubusercontent.com/u/8251344?v=4)](https://github.com/lloricode "lloricode (1 commits)")[![superbiche](https://avatars.githubusercontent.com/u/2478146?v=4)](https://github.com/superbiche "superbiche (1 commits)")

---

Tags

circuit-breakerlaravelphpqueuesresiliencelaravelqueuejobscircuit breakerfuseresilience

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/harris21-laravel-fuse/health.svg)

```
[![Health](https://phpackages.com/badges/harris21-laravel-fuse/health.svg)](https://phpackages.com/packages/harris21-laravel-fuse)
```

###  Alternatives

[propaganistas/laravel-disposable-email

Disposable email validator

6023.2M7](/packages/propaganistas-laravel-disposable-email)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

818355.4k3](/packages/defstudio-telegraph)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5226.7k](/packages/simplestats-io-laravel-client)[iazaran/smart-cache

Smart Cache is a caching optimization package designed to enhance the way your Laravel application handles data caching. It intelligently manages large data sets by compressing, chunking, or applying other optimization strategies to keep your application performant and efficient.

21114.2k](/packages/iazaran-smart-cache)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3915.5k](/packages/rawilk-profile-filament-plugin)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
