Une double facturation, trois emails identiques : et si le coupable n'était pas votre code ?
Quand un client est débité deux fois ou qu'un rapport part en triple exemplaire, le réflexe est de relire la méthode handle() du job. Dans la plupart des cas, elle n'y est pour rien. Le problème vient de trois paramètres que l'on confond trop souvent : retry_after, timeout et backoff. Ils ne vivent pas au même endroit, ne sont pas gérés par la même couche de Laravel et ne répondent pas à la même question. Cet article s'appuie sur Laravel retry_after vs timeout vs backoff: The Three Numbers Behind Duplicate Queue Jobs pour en faire le tour.
retry_after : le bail accordé au worker
Ce paramètre se règle sur la connexion de queue, dans config/queue.php :
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
],
Contrairement à ce que son nom suggère, ce n'est pas un délai avant une nouvelle tentative après échec. C'est un bail : lorsqu'un worker réserve un job, la queue lui laisse retry_after secondes pour le terminer. Passé ce délai sans suppression ni libération du job, la queue en déduit que le worker est mort et remet le job à disposition d'un autre worker.
La conséquence est directe. Si votre job dure légitimement 120 secondes avec un retry_after à 90, un second worker récupère la même tâche à la 90e seconde, alors que le premier travaille toujours. Deux exécutions concurrentes du même traitement : c'est la double facturation.
timeout : le garde-fou côté worker
Le timeout répond à une autre question : combien de temps un job a-t-il le droit de tourner avant d'être interrompu ? Il se définit sur le job (propriété $timeout) ou via l'option --timeout du worker.
class SyncInvoice implements ShouldQueue
{
public int $timeout = 60;
public function handle(): void
{
// ...
}
}
Le worker tue le processus qui dépasse cette limite et marque la tentative comme échouée. C'est un mécanisme de protection contre les jobs bloqués (appel HTTP qui ne répond jamais, requête SQL interminable). Il ne dit rien à la queue sur la manière de redistribuer le job : c'est le rôle de retry_after.
Point important : timeout est appliqué par le worker lui-même, alors que retry_after est appliqué par le système de queue. Les deux horloges tournent indépendamment. Si elles ne sont pas cohérentes, elles se contredisent.
backoff : l'attente entre deux tentatives
Le backoff concerne uniquement ce qui se passe après un échec : combien de temps attendre avant de retenter ?
class SyncInvoice implements ShouldQueue
{
public int $tries = 5;
public function backoff(): array
{
return [10, 30, 120];
}
}
Ici, la deuxième tentative a lieu après 10 secondes, la troisième après 30, puis 120 pour les suivantes. C'est utile pour ne pas marteler un service tiers en difficulté. Mais cela n'a aucun lien avec la détection d'un worker disparu : un job en cours d'exécution n'est jamais concerné par le backoff.
La règle qui évite les conflits
Pour que ces trois valeurs cohabitent, il faut respecter une inégalité simple :
- le
timeoutdu job doit être strictement inférieur auretry_afterde la connexion.
Ainsi, le worker interrompt lui-même un job trop long avant que la queue ne le considère comme abandonné. Avec un timeout de 60 et un retry_after de 90, il reste une marge de 30 secondes. Si les valeurs sont inversées, vous obtenez des exécutions parallèles du même job, puis souvent l'erreur has been attempted too many times, car chaque redistribution consomme une tentative.
Une marge confortable (plusieurs secondes, voire davantage selon la charge) est préférable à un écart de une seconde, car l'arrêt d'un processus n'est pas instantané.
Checklist de vérification
Pour auditer une application existante :
- repérez le job le plus long de chaque queue et mesurez sa durée réelle en production ;
- vérifiez que
retry_afterdépasse largement letimeoutle plus élevé de cette queue ; - si plusieurs queues ont des profils très différents, envisagez des connexions distinctes avec des
retry_afteradaptés ; - définissez un
backoffraisonnable pour les appels à des services externes ; - rendez vos jobs idempotents (clé d'idempotence côté paiement, vérification d'état avant envoi d'email), car même avec une configuration juste, une exécution en double reste possible lors d'un crash réel.
Ce dernier point est la vraie assurance : la configuration réduit les risques, l'idempotence les neutralise.
Conclusion
Trois nombres, trois responsabilités : retry_after détecte un worker mort, timeout borne la durée d'un job, backoff espace les nouvelles tentatives. Les aligner (le timeout toujours sous le retry_after) supprime l'une des sources les plus courantes de doublons en production. Prenez dix minutes pour relire vos fichiers de configuration et vos jobs critiques : c'est souvent là que se cache la cause d'un incident qui semblait inexplicable.
Ce que ça change pour vous
Un même traitement peut être lancé deux fois sans qu'aucune erreur ne soit visible : un client facturé en double, un message envoyé plusieurs fois. La cause est généralement un simple réglage de durée mal accordé entre les différentes parties du système, pas un défaut du travail réalisé. Faire vérifier ces réglages est rapide et peu coûteux, comparé au temps passé à rembourser, à s'excuser auprès des clients et à rétablir leur confiance. Demandez à votre équipe technique ou à votre prestataire de confirmer que ces paramètres ont été contrôlés sur vos traitements automatiques les plus sensibles.
Et votre site, il vaut quoi ?
Vitesse, accessibilité, référencement technique. Le rapport est écrit pour un dirigeant, pas pour un développeur. Gratuit, sans inscription.