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.

                
.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.

                
.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

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

                
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()]); } }

Besoin d'aide sur votre projet ?

Je peux vous accompagner dans le développement ou l'optimisation de votre application.

Me contacter