Aller au contenu principal
Site en cours de refonte — quelques pages peuvent bouger ou évoluer.
18 mars 2026

Laravel Queues : le batch qui “oublie” ta queue custom (et balance un job sur default)

Le pire bug, c’est celui qui “marche”… mais pas sur la bonne queue. Avec un batch Laravel + array chain, le premier job peut partir sur la queue par défaut sans prévenir.

8 min de lecture
7 vues
réactions
Partager :
Laravel Queues : le batch qui “oublie” ta queue custom (et balance un job sur default)

Tu as une app Laravel un peu sérieuse, donc tu as des queues séparées. Une pour les emails, une pour les exports, une pour le billing, une pour les trucs lents. Et puis un jour, tu lances un Bus::batch() “proprement”, avec une chaîne de jobs… et le premier job part sur default au lieu de ta queue dédiée.

Pas d’erreur. Pas de warning. Juste un routage de queue qui te flingue l’ops: mauvais workers, mauvaises priorités, Horizon qui montre un truc “bizarre”, et parfois un SLA qui explose alors que “tout marche”. Ce bug a été identifié et corrigé côté framework dans un PR Laravel. Ce qui m’intéresse ici, c’est le scénario réel, comment le reproduire, et comment ne plus te faire avoir.

Le symptôme en prod : un job « prioritaire » exécuté par les mauvais workers

Dans la vraie vie, ça ressemble rarement à « le job a échoué ». Ça ressemble plutôt à « pourquoi le worker default se met à mouliner à 100% alors que la queue emails est calme ? ». Ou l’inverse: tes workers dédiés à billing ne voient rien, pendant que des jobs censés être isolés tournent sur default et ralentissent tout le reste.

Le côté perfide, c’est que ça peut ne toucher qu’un seul job. Typiquement le premier d’une chaîne. Donc tu as l’impression que “globalement” ça respecte la queue, sauf ce petit dérapage. Et ce petit dérapage suffit pour créer un incident si ton premier job est celui qui fait l’appel API coûteux, le calcul lourd, ou le lock transactionnel.

Reproduction minimale : Bus::batch + array chain = queue perdue sur le 1er job

Le cas problématique, c’est quand tu mets une « chain » sous forme de tableau dans un batch. En Laravel, on peut exprimer une chaîne de jobs en passant un tableau (ce qu’on appelle souvent une array chain). Le bug: si le batch n’a pas de queue explicite, le premier job de cette chaîne peut se retrouver avec une queue remise à null. Résultat: il retombe sur la queue par défaut du worker.

use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;

Bus::batch([
    [
        (new \App\Jobs\PrepareExport($exportId))->onQueue('exports'),
        (new \App\Jobs\GenerateExport($exportId))->onQueue('exports'),
    ],
])
->catch(function (Batch $batch, Throwable $e) {
    // ...
})
->dispatch();

Ce que tu penses que tu as demandé: les deux jobs sur exports. Ce que tu peux obtenir selon la version et le cas précis: le premier job sur default, le second sur exports. Oui, c’est exactement le genre de truc qui te fait remettre en question ta santé mentale quand tu regardes Horizon.

Pourquoi ça arrive : un détail d’implémentation dans Batch::add()

Sans rentrer dans le code interne ligne par ligne, l’idée est simple: quand Laravel ajoute des jobs à un batch, il “normalise” et enrichit les jobs (batch id, callbacks, etc.). Dans le cas spécifique d’une chaîne exprimée en tableau, il y avait un effet de bord où l’assignation de queue du premier job se faisait écraser quand le batch lui-même n’était pas attaché à une queue.

Le résultat concret, c’est cette valeur de queue qui repasse à null à un moment où tu ne t’y attends pas. Et comme null signifie “prends la queue par défaut du driver / worker”, tu te retrouves avec un job qui part ailleurs, sans bruit.

Pourquoi c’est grave : ça ne casse pas la CI, ça casse l’exploitation

Ce bug ne va pas forcément faire tomber des tests. Ton job s’exécute. Ton batch finit. Tout le monde est content. Sauf que tu as perdu le bénéfice principal d’une architecture de queues séparées: l’isolation.

Quand un job “lourd” atterrit sur default, il peut retarder tout ce qui est derrière. Et ce derrière, c’est souvent ce que tu oublies: notifications, webhooks, sync CRM, tâches système. À l’inverse, si tu as des workers dédiés très chers (gros CPU pour exports, gros timeout pour scraping, etc.), tu payes pour rien pendant que default prend la charge.

Et côté incident, c’est pénible: tu enquêtes sur « pourquoi ça lag », tu vois un batch “normal”, tu cherches du côté des retries, de Redis, des timeouts… alors que c’est juste un routage de queue qui s’est fait écraser.

Comment le repérer vite : Horizon, logs et métriques par queue

Horizon est ton meilleur ami ici, parce qu’il te montre clairement le triptyque job / queue / timestamp. Quand tu suspectes ce bug, regarde le tout début d’un batch: si tu vois systématiquement un premier job sur default alors que la suite est sur exports (ou emails, etc.), tu tiens un signal fort.

Côté logs, je suis fan d’un truc tout simple en prod: loguer explicitement la queue au démarrage du handle() des jobs critiques, au moins le temps d’une investigation. Parce que Laravel/Horizon te dira “où il a tourné”, mais si tu as des retries ou des redirections internes, tu veux une source de vérité dans le contexte d’exécution.

Et si tu fais un minimum d’observabilité, une métrique « jobs exécutés par queue » (ou juste des tags par queue dans ton APM) te donne un graphe qui crie. Un pic sur default pile au moment d’un export, c’est rarement un hasard.

Contournements propres : ce que je ferais avant même de bump Laravel

Le contournement le plus robuste, c’est d’assigner la queue au niveau du batch, pas seulement au niveau des jobs. Comme ça, même si un job perd son attribut, tu as un filet de sécurité. Et surtout, tu exprimes vraiment ton intention: « ce batch est un batch d’exports ».

Bus::batch([
    [
        new \App\Jobs\PrepareExport($exportId),
        new \App\Jobs\GenerateExport($exportId),
    ],
])
->onQueue('exports')
->dispatch();

Deuxième contournement (quand tu veux être parano): éviter les array chains dans les batchs si tu n’en as pas besoin. Parfois, tu veux juste exécuter plusieurs jobs, pas forcément chaînés. Et si tu veux vraiment une chaîne, tu peux la créer différemment selon ton design, ou envelopper la logique dans un job orchestrateur qui lui est sur la bonne queue et enchaîne ensuite.

Enfin, j’ajouterais un test de non-régression sur les jobs sensibles. Pas un test “le job s’exécute”, un test “le job est dispatché sur la bonne queue”. C’est le genre de test que tu n’écris pas par plaisir, mais qui te sauve quand tu touches aux versions, au driver (Redis/SQS), ou à la config Horizon.

À surveiller pendant un upgrade : versions Laravel, codebase et config workers

Le piège des bugs de queue, c’est que tu peux upgrader Laravel “tranquillement” et découvrir le problème après coup, parce que c’est l’ordonnancement qui change, pas ton code métier. Donc quand tu bumps une version qui touche aux batches/queues, rejoue un scénario réaliste: un batch qui envoie des jobs sur une queue non-default, avec un supervisor Horizon qui n’écoute pas default (ou l’inverse). C’est là que tu vois immédiatement si ça part au mauvais endroit.

Et vérifie ta configuration de workers: si tu as des workers qui écoutent plusieurs queues, un job mal routé peut passer “inaperçu” et tu ne le verras qu’au moment où tu sépares plus strictement. Beaucoup d’équipes découvrent ce genre de bug le jour où elles professionnalisent Horizon et isolent vraiment les files.

Mon avis (assez tranché) : la queue, ce n’est pas un détail d’implémentation

En Laravel, on a tendance à voir la queue comme un truc “à côté”: on met ->onQueue('emails') et on passe à autre chose. Sauf qu’en prod, c’est une décision d’architecture. Ça définit qui exécute quoi, avec quel timeout, quelle concurrence, quels coûts, et quelle priorité.

Donc oui, un bug qui “oublie” une queue sur un job, même si ça ne casse aucun test et que ça ne lève aucune exception, c’est un vrai bug de fiabilité. Et si tu te dis « ça n’arrivera pas chez nous », je te souhaite de ne jamais faire un export qui dure 4 minutes sur la même queue que tes notifications transactionnelles.

Sources

Cet article vous a plu ?

Commentaires

Laisser un commentaire

Entre 10 et 2000 caractères

Les commentaires sont modérés avant publication.

Aucun commentaire pour le moment.

Soyez le premier à donner votre avis !