Aller au contenu principal
Making DigitalMaking Digital
Note technique

Files d'attente Laravel en production : Redis, Supervisor, retries et jobs en échec

En bref

En production, une file Laravel sur Redis a besoin d'un superviseur (Supervisor) qui relance les workers, d'un `timeout` inférieur à `retry_after`, de tentatives et délais maîtrisés (`tries`, `backoff`), d'un nettoyage des jobs en échec et d'un `queue:restart` à chaque déploiement.

Une file d'attente qui marche sur votre poste peut se comporter autrement sur un serveur : worker qui s'arrête sans bruit, job rejoué deux fois, jobs en échec qui s'accumulent, code qui n'est pas pris en compte après un déploiement. Voici les réglages qui évitent ces cas, d'après la documentation de Laravel.

Pourquoi un worker doit-il être supervisé ?

php artisan queue:work est un processus long. Il s'arrête quand un job dépasse son délai, après queue:restart ou si le serveur redémarre. Il faut donc un gestionnaire de processus qui le relance, en général Supervisor :

INI
; /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600

numprocs fixe le nombre de workers en parallèle : sur un petit serveur, deux suffisent souvent. --max-time=3600 fait quitter le worker au bout d'une heure pour libérer la mémoire accumulée. stopwaitsecs doit être supérieur à la durée de votre job le plus long, sinon Supervisor tue le job en cours. Puis :

Terminal
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"

Quelles valeurs choisir pour Redis, retry_after et timeout ?

Dans config/queue.php, la connexion Redis a un paramètre retry_after (90 secondes par défaut). Au bout de ce délai, un job pas terminé est considéré comme perdu et remis dans la file. Le timeout du job (60 secondes par défaut) doit donc toujours être plus court que retry_after, sinon un second worker reprend le job alors que le premier travaille encore : doublon.

PHP
// config/queue.php
'redis' => [
    'driver' => 'redis',
    'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
    'queue' => env('REDIS_QUEUE', 'default'),
    'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
    'block_for' => 5,
    'after_commit' => false,
],

block_for fait attendre le worker quelques secondes côté Redis au lieu d'interroger en boucle. Le timeout d'un job demande l'extension PHP PCNTL. En mode Redis Cluster, le nom de la file doit contenir une accolade de hachage, par exemple {default}.

Comment régler les tentatives et le délai entre deux essais ?

Un appel à une API distante échoue de temps en temps : mieux vaut réessayer en espaçant les essais que de rejouer immédiatement. Les réglages se posent sur le job, et priment sur ceux de la ligne de commande :

PHP
// app/Jobs/SyncOrderToErp.php
namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;

#[Tries(5)]
#[Backoff([10, 60, 300])]
#[Timeout(60)]
class SyncOrderToErp implements ShouldQueue
{
    use Queueable;

    // ...
}

Ici, le job est tenté 5 fois, avec 10 s, puis 60 s, puis 300 s d'attente (le dernier délai se répète). Avant Laravel 13, on écrit les mêmes réglages en propriétés (public $tries, public $backoff, public $timeout).

Un job qui peut être rejoué doit pouvoir l'être sans rien dupliquer (facture, e-mail). Pour éviter deux exécutions simultanées sur le même objet, utilisez ShouldBeUnique ou le middleware WithoutOverlapping.

Que faire des jobs en échec ?

Un job qui épuise ses tentatives est enregistré dans la table failed_jobs (migration livrée avec Laravel, sinon php artisan make:queue-failed-table). Surveillez-la au lieu de la laisser grossir :

Terminal
php artisan queue:failed
php artisan queue:retry <id>
php artisan queue:retry all
php artisan queue:prune-failed --hours=48

Planifiez queue:prune-failed (par exemple chaque jour) pour supprimer les échecs anciens, et lisez les erreurs avant de relancer : relancer en boucle un job qui échoue pour une raison permanente ne sert à rien.

Que faire à chaque déploiement ?

Un worker garde l'ancien code en mémoire. Après chaque mise en production :

Terminal
php artisan queue:restart

Les workers finissent leur job en cours puis quittent, et Supervisor les relance avec le nouveau code. C'est aussi le bon réflexe après un changement de configuration, par exemple quand env() renvoie null à cause des caches, ou après une mise à jour de PHP.

À retenir
  • Supervisor relance les workers : numprocs, --max-time et un stopwaitsecs plus long que le job le plus long.
  • timeout du job toujours inférieur à retry_after, sinon doublons.
  • #[Tries] et #[Backoff] pour espacer les essais, jobs idempotents.
  • queue:prune-failed planifié, et queue:restart à chaque déploiement.

Source : documentation officielle des files d'attente de Laravel.

Questions fréquentes

Pourquoi mon job est-il exécuté deux fois ?

Souvent parce que son timeout dépasse retry_after : la file le remet à disposition alors que le premier worker travaille encore. Gardez le timeout plus court que retry_after, et rendez le job idempotent.

Combien de workers faut-il lancer ?

Autant que de jobs à traiter en parallèle sans saturer le serveur. Sur un petit serveur, deux workers (numprocs=2) suffisent souvent pour commencer ; augmentez si la file s'allonge.

Mon code ne change pas après un déploiement, pourquoi ?

Les workers sont des processus longs qui gardent l'ancien code en mémoire. Lancez php artisan queue:restart après chaque déploiement : Supervisor les relancera avec le nouveau code.

Making Digital

Nous connectons les logiciels des PME, automatisons leurs tâches répétitives et créons des assistants IA, depuis 2010. Nos articles partent de nos projets réels.

Notre parcours
Une question sur ce sujet ?

Parlons-en 30 minutes.

Vous saurez si votre problème se règle, comment, et en combien de temps. Sans engagement.