Vous souhaitez nous soutenir ? Devenez sponsor de l'association sur notre page Github

Protégez vos files d'attente avec Laravel Fuse

Publié le 26 mars 2026 par Ludovic Guénet
Couverture de l'article Protégez vos files d'attente avec Laravel Fuse

Stripe est down. Et vos 10 000 jobs Laravel continuent de s'exécuter, d'attendre 30 secondes en timeout, de retry, de re-timeout... jusqu'à ce que votre queue soit complètement figée pour les 25 prochaines heures.

C'est exactement le problème que résout Laravel Fuse.

Le problème : la cascade d'échecs

Dans une application Laravel en production, vos jobs communiquent souvent avec des services tiers : API de paiement, service d'emailing, microservices internes. Ces services peuvent tomber. Et quand ils tombent, sans protection, voici ce qui se passe :

  • Chaque job tente d'appeler le service
  • Il attend le timeout (souvent 30 secondes)
  • Il échoue, retry, retimeout
  • Des centaines de jobs s'accumulent dans la queue
  • Vos workers sont saturés pour rien
  • Vos vraies priorités ne passent plus

La solution connue dans le monde du génie logiciel s'appelle le Circuit Breaker pattern.

Le pattern Circuit Breaker

Le concept vient littéralement de l'électricité. Un disjoncteur surveille le courant et coupe le circuit si quelque chose cloche, pour éviter la surtension.

En logiciel, c'est pareil. Le circuit breaker surveille le taux d'échec de vos appels vers un service tiers et passe par trois états :

CLOSED — Tout fonctionne normalement. Les jobs s'exécutent, le courant passe.

OPEN — Trop d'échecs ont été détectés. Le circuit s'ouvre et les jobs échouent instantanément (en 1ms, pas 30 secondes) sans même appeler le service externe. Ils sont remis en attente automatiquement pour être rejoués plus tard.

HALF-OPEN — Après un délai configurable, le système envoie une requête "sonde" pour tester si le service est revenu. Si oui, le circuit repasse en CLOSED. Sinon, retour en OPEN.

Laravel Fuse implémente ce pattern directement dans votre stack Laravel, sans dépendance externe.

Installation

1composer require harris21/laravel-fuse
2php artisan vendor:publish --tag=fuse-config

Intégration dans un job

L'intégration se fait via un middleware sur votre job :

1use Harris21\Fuse\Middleware\CircuitBreakerMiddleware;
2 
3class ChargeCustomer implements ShouldQueue
4{
5 public $tries = 0; // Unlimited releases
6 public $maxExceptions = 3; // Seules les vraies erreurs comptent
7 
8 public function middleware(): array
9 {
10 return [new CircuitBreakerMiddleware('stripe')];
11 }
12 
13 public function handle(): void
14 {
15 Stripe::charges()->create([...]);
16 }
17}

C'est tout. Votre job est maintenant protégé.

Configuration

Le fichier config/fuse.php vous permet de paramétrer finement le comportement par service :

1return [
2 'default_threshold' => 50, // % d'échec pour déclencher l'ouverture
3 'default_timeout' => 60, // Secondes avant de tester la reprise
4 'default_min_requests' => 10, // Minimum de requêtes avant d'évaluer
5 
6 'services' => [
7 'stripe' => [
8 'threshold' => 50,
9 'timeout' => 30,
10 'min_requests' => 5,
11 
12 // Tolérance plus haute en heures de pointe
13 'peak_hours_threshold' => 60,
14 'peak_hours_start' => 9,
15 'peak_hours_end' => 17,
16 ],
17 'mailgun' => [
18 'threshold' => 60,
19 'timeout' => 120,
20 'min_requests' => 10,
21 ],
22 ],
23];

Chaque service dispose de son propre circuit breaker indépendant. Si Mailgun tombe, Stripe n'est pas impacté.

Classification intelligente des erreurs

Toutes les erreurs ne signifient pas que le service est down. Fuse fait la distinction :

✅ Erreurs comptabilisées — le service est probablement en cause :

  • 500, 502, 503 — le serveur a un problème
  • Timeout de connexion — le service est injoignable

❌ Erreurs ignorées — le service fonctionne, le problème vient d'ailleurs :

  • 429 Too Many Requests — c'est vous qui spammez
  • 401 Unauthorized — problème de configuration
  • 403 Forbidden — problème de permissions

Concrètement : si vous atteignez la rate limit de Stripe, le circuit ne s'ouvre pas. Stripe fonctionne, c'est votre code qui envoie trop de requêtes.

Classification personnalisée

Certains services ont des comportements non-standard. Stripe, par exemple, retourne parfois un 500 pour des erreurs d'idempotence qui n'ont rien à voir avec une panne. Vous pouvez créer votre propre classifier :

1namespace App\Fuse;
2 
3use Harris21\Fuse\Classifiers\DefaultFailureClassifier;
4use GuzzleHttp\Exception\ServerException;
5use Throwable;
6 
7class StripeFailureClassifier extends DefaultFailureClassifier
8{
9 public function shouldCount(Throwable $e): bool
10 {
11 if ($e instanceof ServerException) {
12 $body = (string) $e->getResponse()?->getBody();
13 
14 if (str_contains($body, 'idempotency')) {
15 return false; // Pas une vraie panne
16 }
17 }
18 
19 return parent::shouldCount($e);
20 }
21}

Puis dans la config :

1'stripe' => [
2 'threshold' => 50,
3 'failure_classifier' => \App\Fuse\StripeFailureClassifier::class,
4],

Écouter les événements

Fuse dispatche des événements Laravel à chaque transition d'état. Pratique pour vos alertes Slack ou vos logs :

1use Harris21\Fuse\Events\CircuitBreakerOpened;
2 
3class AlertOnCircuitOpen
4{
5 public function handle(CircuitBreakerOpened $event): void
6 {
7 Log::critical("Circuit ouvert pour {$event->service}", [
8 'failure_rate' => $event->failureRate,
9 'attempts' => $event->attempts,
10 'failures' => $event->failures,
11 ]);
12 
13 // Notifier l'équipe via Slack, PagerDuty, etc.
14 }
15}

Les trois événements disponibles sont CircuitBreakerOpened, CircuitBreakerHalfOpen et CircuitBreakerClosed.

Dashboard de monitoring en temps réel

Fuse embarque une page de statut intégrée. Activez-la dans votre .env :

1FUSE_STATUS_PAGE_ENABLED=true

Elle est accessible sur /fuse et affiche en temps réel l'état de chaque circuit, le taux d'échec, l'historique des transitions et l'heure de la prochaine tentative de reprise. Elle se rafraîchit automatiquement toutes les 2 secondes.

L'accès est protégé par une gate Laravel viewFuse, que vous pouvez surcharger dans votre AppServiceProvider :

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

Utilisation directe (hors jobs)

Le circuit breaker peut aussi être utilisé en dehors de la queue, pour des appels synchrones :

1use Harris21\Fuse\CircuitBreaker;
2 
3$breaker = new CircuitBreaker('stripe');
4 
5if (!$breaker->isOpen()) {
6 try {
7 $result = Stripe::charges()->create([...]);
8 $breaker->recordSuccess();
9 return $result;
10 } catch (Exception $e) {
11 $breaker->recordFailure($e);
12 throw $e;
13 }
14} else {
15 // Stratégie de fallback
16 return $this->fallbackResponse();
17}

Stratégies de fallback

Quand le circuit s'ouvre, votre application a besoin d'un plan B. Quelques approches courantes :

Données en cache — Retourner la dernière valeur connue. Des données légèrement obsolètes valent mieux qu'une erreur 500.

Service de secours — Basculer sur un prestataire alternatif, ou marquer la commande "en attente" pour traitement ultérieur.

Dégradation gracieuse — Masquer la fonctionnalité si elle n'est pas critique. Une page qui charge sans les recommandations vaut mieux qu'une page blanche.

Re-queue différé — Fuse le fait déjà nativement avec release(). Vos jobs sont remis en attente, pas perdus définitivement.

Prérequis

  • PHP 8.3+
  • Laravel 11+
  • Redis recommandé en production (le cache fichier peut avoir des race conditions lors de la phase HALF-OPEN)

Conclusion

Laravel Fuse apporte une solution élégante et native à un problème qui peut paralyser une application en production. Le setup est minimal, la configuration est flexible, et le comportement est automatique une fois en place.

Sans lui : 10 000 jobs × 30 secondes de timeout = plus de 25 heures pour vider la queue.
Avec lui : le circuit s'ouvre après quelques échecs, la queue se vide en quelques secondes, et la reprise est automatique dès que le service revient.

Le package a été créé par Harris Raftopoulos et présenté à Laracon India 2026. Le code source est disponible sur GitHub sous licence MIT.

1composer require harris21/laravel-fuse
Source : https://github.com/harris21/laravel-fuse
Ludovic Guénet avatar

Écrit par

Ludovic Guénet

software engineer • mentor • bassist

Aucun commentaire

Tu veux commenter ? Crée un compte ou connecte-toi.

A lire

Autres articles de la même catégorie