Laravel Queues : guide complet pour la production
Les queues sont essentielles pour une application performante : emails, notifications, traitements lourds... Ce guide couvre tout ce qu'il faut savoir pour les utiliser efficacement en production.
Pourquoi utiliser les queues ?
Une règle simple : tout ce qui n'a pas besoin d'être fait immédiatement doit être en queue. Cela améliore le temps de réponse perçu par l'utilisateur et la résilience de l'application.
Cas d'usage typiques
Emails et notifications : l'envoi peut prendre plusieurs secondes.
Traitement d'images : redimensionnement, compression.
Appels API externes : webhooks, synchronisations.
Exports PDF/Excel : génération de rapports.
Batch processing : import de données massives.
Sans queue (mauvais)
// L'utilisateur attend 3-5 secondes
public function register(Request $request)
{
$user = User::create($request->validated());
Mail::to($user)->send(new WelcomeEmail($user)); // Bloquant !
return redirect('/dashboard');
}
Avec queue (bien)
// Réponse instantanée, email envoyé en background
public function register(Request $request)
{
$user = User::create($request->validated());
Mail::to($user)->queue(new WelcomeEmail($user)); // Non-bloquant
return redirect('/dashboard');
}
Redis vs Database : quel driver choisir ?
Driver Database
Simple à mettre en place, ne nécessite aucune infrastructure supplémentaire.
- Avantages : zéro config, persistance garantie, transactions ACID
- Inconvénients : plus lent, charge la DB, pas de pub/sub
- Quand l'utiliser : projets simples, faible volume de jobs
.env
QUEUE_CONNECTION=database
Terminal
php artisan queue:table
php artisan migrate
Driver Redis
La solution recommandée pour la production. Rapide, scalable, et permet Horizon.
- Avantages : très performant, supporte Horizon, pub/sub natif
- Inconvénients : infrastructure supplémentaire, risque de perte si crash
- Quand l'utiliser : production, volume important, besoin de monitoring
.env
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
Ma recommandation
Dev/staging : database driver, simple et suffisant.
Production : Redis + Horizon, monitoring et performance.
Créer et dispatcher des jobs
Structure d'un job
app/Jobs/ProcessOrderJob.php
namespace App\Jobs;
use App\Models\Order;
use App\Services\PaymentService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessOrderJob implements ShouldQueue
{
use Queueable;
// Nombre de tentatives max
public int $tries = 3;
// Timeout en secondes
public int $timeout = 120;
// Délai entre les tentatives (backoff exponentiel)
public array $backoff = [10, 60, 300];
public function __construct(
public Order $order,
) {}
public function handle(PaymentService $paymentService): void
{
$paymentService->capture($this->order);
$this->order->update(['status' => 'completed']);
}
// Appelé si toutes les tentatives échouent
public function failed(Throwable $exception): void
{
Log::error('Order processing failed', [
'order_id' => $this->order->id,
'error' => $exception->getMessage(),
]);
$this->order->update(['status' => 'failed']);
}
}
Dispatcher un job
Différentes façons de dispatcher
// Dispatch simple
ProcessOrderJob::dispatch($order);
// Avec délai
ProcessOrderJob::dispatch($order)
->delay(now()->addMinutes(5));
// Sur une queue spécifique
ProcessOrderJob::dispatch($order)
->onQueue('payments');
// Uniquement si pas déjà en queue (évite les doublons)
ProcessOrderJob::dispatchIf($order->isPending(), $order);
// Après la réponse HTTP (pour les trucs rapides)
ProcessOrderJob::dispatchAfterResponse($order);
Stratégies de retry
Un job peut échouer pour de nombreuses raisons : timeout réseau, API externe down, race condition... Une bonne stratégie de retry est cruciale.
Backoff exponentiel
Augmenter le délai entre chaque tentative évite de surcharger un service déjà en difficulté.
Backoff exponentiel
class SyncWithExternalApiJob implements ShouldQueue
{
public int $tries = 5;
// 10s, 30s, 90s, 270s, 810s
public function backoff(): array
{
return [10, 30, 90, 270, 810];
}
}
Échouer immédiatement sur certaines erreurs
Certaines erreurs ne méritent pas de retry : authentification invalide, ressource supprimée...
Middleware FailOnException
use Illuminate\Queue\Middleware\FailOnException;
use App\Exceptions\InvalidApiKeyException;
use App\Exceptions\ResourceNotFoundException;
class SyncUserJob implements ShouldQueue
{
public int $tries = 3;
public function middleware(): array
{
return [
// Pas de retry sur ces erreurs
new FailOnException([
InvalidApiKeyException::class,
ResourceNotFoundException::class,
]),
];
}
}
Limiter le nombre d'exceptions
maxExceptions vs tries
class ProcessPodcastJob implements ShouldQueue
{
// 25 tentatives max (inclut les release manuels)
public int $tries = 25;
// Mais échoue après 3 exceptions non catchées
public int $maxExceptions = 3;
public function handle(): void
{
Redis::throttle('podcast-api')
->allow(10)
->every(60)
->then(
fn() => $this->process(),
fn() => $this->release(30) // Rate limited, réessayer
);
}
}
Workers en production
Démarrer un worker
Terminal
# Worker basique
php artisan queue:work
# Avec options production
php artisan queue:work redis --tries=3 --timeout=60 --queue=high,default,low
# Redémarrer automatiquement après déploiement
php artisan queue:restart
Ne jamais utiliser queue:listen en prod
queue:listen reboot le framework à chaque job (lent). Utilisez queue:work qui garde le framework en mémoire.
Supervisor pour la fiabilité
En production, les workers doivent être gérés par un process manager comme Supervisor qui les redémarre automatiquement.
/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=4
redirect_stderr=true
stdout_logfile=/var/log/supervisor/worker.log
stopwaitsecs=3600
Terminal
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
Horizon : le must-have pour Redis
Laravel Horizon fournit un dashboard, du monitoring, et une gestion avancée des workers pour les queues Redis.
Installation
composer require laravel/horizon
php artisan horizon:install
php artisan migrate
Configuration par environnement
config/horizon.php
'environments' => [
'production' => [
'supervisor-default' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
'tries' => 3,
'timeout' => 60,
],
'supervisor-high' => [
'connection' => 'redis',
'queue' => ['high'],
'balance' => 'simple',
'processes' => 5,
'tries' => 1,
'timeout' => 30,
],
],
'local' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default', 'high'],
'balance' => 'simple',
'processes' => 3,
],
],
],
Stratégies de balancing
- simple : répartit les workers équitablement entre les queues
- auto : ajuste dynamiquement selon la charge (recommandé)
- false : pas de balancing, traite les queues dans l'ordre
Alertes sur les longues attentes
config/horizon.php
// Alerte si un job attend plus de X secondes
'waits' => [
'redis:high' => 30, // Critique
'redis:default' => 60, // Normal
'redis:low' => 300, // Peut attendre
],
Terminal - Lancer Horizon
php artisan horizon
Dashboard Horizon
Accessible sur /horizon. En production, protégez-le avec Horizon::auth() dans HorizonServiceProvider.
Gestion des jobs échoués
Commandes utiles
# Voir les jobs échoués
php artisan queue:failed
# Retry un job spécifique
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece
# Retry tous les jobs d'une queue
php artisan queue:retry --queue=payments
# Retry tous les jobs échoués
php artisan queue:retry all
# Supprimer un job échoué
php artisan queue:forget ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece
# Purger les vieux jobs échoués (+ de 48h)
php artisan queue:flush --hours=48
Pruning automatique
app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
// Nettoyer les jobs échoués de plus de 7 jours
$schedule->command('queue:flush --hours=168')->daily();
// Pour Horizon : prune des métriques
$schedule->command('horizon:snapshot')->everyFiveMinutes();
}
Bonnes pratiques production
Checklist production
- Utilisez Redis + Horizon pour le monitoring et l'auto-scaling
- Timeout < retry_after : évite les jobs traités en double
- Backoff exponentiel : ne surchargez pas les services en difficulté
- Jobs idempotents : un job rejoué ne doit pas créer de doublons
- Sérialisez les IDs, pas les objets : évite les données stales
- Logs structurés : context avec job_id, order_id, etc.
- Alertes sur queue:failed : soyez notifié des échecs
- max-time sur les workers : évite les memory leaks
Job idempotent
class SendInvoiceEmailJob implements ShouldQueue
{
public function handle(): void
{
// Idempotent : vérifie si déjà envoyé
if ($this->invoice->email_sent_at) {
return;
}
Mail::to($this->invoice->customer)
->send(new InvoiceMail($this->invoice));
$this->invoice->update(['email_sent_at' => now()]);
}
}