TL;DR
Laravel's queue system can silently drop failed jobs under certain conditions. Implement proper monitoring, set appropriate retry limits, and always have a dead letter queue strategy.
The problem: silent failures
Last month, I was debugging a client's order processing system. They had a classic setup: user places order, job gets dispatched to process payment and send confirmation. Queue workers running via Supervisor. Horizon dashboard showing everything green.
Except customers weren't getting their confirmation emails. Some orders weren't being processed at all. And the monitoring said everything was fine.
Here's what was happening: their jobs were failing, but not in a way that Laravel would catch.
How jobs can fail silently
Laravel's queue system is robust, but it has some assumptions baked in that can bite you:
1. Timeout deaths
When a job exceeds its timeout, the worker process kills it. But depending on your configuration, this might not trigger the failed() method on your job:
// This job will be killed after 60 seconds
class ProcessOrder implements ShouldQueue
{
public $timeout = 60;
public function handle()
{
// If this takes > 60 seconds, the job dies
// But failed() might never be called
$this->processPayment();
}
public function failed(Throwable $e)
{
// This might not be called on timeout!
Log::error('Order failed', ['order' => $this->order->id]);
}
}
2. Memory limit kills
Same issue with memory. If your worker hits the memory limit, PHP dies. No exception, no failed handler, nothing.
3. The "processed" lie
Horizon and other monitoring tools typically track:
- Pending: Jobs in the queue
- Completed: Jobs that finished without throwing an exception
- Failed: Jobs that threw an exception and exceeded retry attempts
But "completed" doesn't mean "successful." A job can complete with partial work done, with silent errors logged, or with business logic failures that don't throw exceptions.
The solution: trust but verify
1. Implement job-level verification
Don't rely on the queue system to tell you if a job succeeded. Track it yourself:
class ProcessOrder implements ShouldQueue
{
public function handle()
{
$order = $this->order;
// Mark as processing
$order->update(['processing_status' => 'processing']);
try {
$this->processPayment();
$this->sendConfirmation();
// Mark as completed
$order->update([
'processing_status' => 'completed',
'processed_at' => now(),
]);
} catch (Throwable $e) {
$order->update(['processing_status' => 'failed']);
throw $e;
}
}
}
2. Add a watchdog query
Run a scheduled command that looks for orphaned jobs:
// In your scheduler
$schedule->command('orders:check-stuck')->everyFiveMinutes();
// The command
$stuckOrders = Order::where('processing_status', 'processing')
->where('updated_at', '<', now()->subMinutes(10))
->get();
foreach ($stuckOrders as $order) {
Log::warning('Stuck order detected', ['order' => $order->id]);
// Alert, retry, or manual intervention
}
3. Set sensible retry limits
Don't let jobs retry forever. Set explicit limits and handle the failure case:
class ProcessOrder implements ShouldQueue
{
public $tries = 3;
public $backoff = [60, 300, 900]; // 1min, 5min, 15min
public function failed(Throwable $e)
{
// This WILL be called after all retries exhausted
$this->order->update(['processing_status' => 'failed_permanently']);
// Notify someone
Notification::send(
User::admins()->get(),
new OrderFailedNotification($this->order, $e)
);
}
}
The bigger picture
Queue monitoring isn't just about watching Horizon. It's about having multiple layers of verification:
- Application-level tracking: Track job state in your database
- Watchdog processes: Actively look for stuck or orphaned work
- Business metrics: Monitor outcomes, not just job counts
- Alerting: Get notified when things go wrong, not when you check the dashboard
Takeaway
The queue is a tool, not a guarantee. Trust the system to do its job, but verify that your business logic actually completed successfully. The few minutes spent implementing proper tracking will save you hours of debugging silent failures.
Need help with queue architecture or debugging silent failures? Let's talk.
Het probleem: stille fouten
Vorige maand was ik bezig met het debuggen van een orderverwerking systeem van een klant. Ze hadden een klassieke setup: gebruiker plaatst order, job wordt verzonden om betaling te verwerken en bevestiging te sturen. Queue workers draaien via Supervisor. Horizon dashboard toont alles groen.
Behalve dat klanten geen bevestigingsmails kregen. Sommige orders werden helemaal niet verwerkt. En de monitoring zei dat alles in orde was.
Dit was wat er gebeurde: hun jobs faalden, maar niet op een manier die Laravel zou opvangen.
Hoe jobs stilletjes kunnen falen
Laravel's queue systeem is robuust, maar het heeft enkele aannames ingebakken die je kunnen bijten:
1. Timeout deaths
Wanneer een job zijn timeout overschrijdt, beëindigt het worker proces het. Maar afhankelijk van je configuratie, kan dit de failed() methode op je job niet activeren:
// Deze job wordt beëindigd na 60 seconden
class ProcessOrder implements ShouldQueue
{
public $timeout = 60;
public function handle()
{
// Als dit > 60 seconden duurt, sterft de job
// Maar failed() wordt misschien nooit aangeroepen
$this->processPayment();
}
public function failed(Throwable $e)
{
// Dit wordt mogelijk niet aangeroepen bij timeout!
Log::error('Order gefaald', ['order' => $this->order->id]);
}
}
2. Geheugenlimiet kills
Hetzelfde probleem met geheugen. Als je worker de geheugenlimiet bereikt, sterft PHP. Geen exceptie, geen failed handler, niets.
3. De "verwerkt" leugen
Horizon en andere monitoring tools volgen typisch:
- Pending: Jobs in de queue
- Completed: Jobs die zijn afgerond zonder een exceptie te gooien
- Failed: Jobs die een exceptie gooiden en het aantal retry pogingen overschreden
Maar "completed" betekent niet "succesvol". Een job kan compleet zijn met gedeeltelijk werk gedaan, met stille fouten gelogd, of met business logic fouten die geen excepties gooien.
De oplossing: vertrouw maar verifieer
1. Implementeer job-level verificatie
Vertrouw niet op het queue systeem om je te vertellen of een job is geslaagd. Volg het zelf:
class ProcessOrder implements ShouldQueue
{
public function handle()
{
$order = $this->order;
// Markeer als verwerkend
$order->update(['processing_status' => 'processing']);
try {
$this->processPayment();
$this->sendConfirmation();
// Markeer als voltooid
$order->update([
'processing_status' => 'completed',
'processed_at' => now(),
]);
} catch (Throwable $e) {
$order->update(['processing_status' => 'failed']);
throw $e;
}
}
}
2. Voeg een watchdog query toe
Draai een gepland commando dat zoekt naar verweesde jobs:
// In je scheduler
$schedule->command('orders:check-stuck')->everyFiveMinutes();
// Het commando
$stuckOrders = Order::where('processing_status', 'processing')
->where('updated_at', '<', now()->subMinutes(10))
->get();
foreach ($stuckOrders as $order) {
Log::warning('Vastgelopen order gedetecteerd', ['order' => $order->id]);
// Alert, retry, of handmatige interventie
}
3. Stel verstandige retry limieten in
Laat jobs niet voor altijd opnieuw proberen. Stel expliciete limieten in en handel het faalscenario af:
class ProcessOrder implements ShouldQueue
{
public $tries = 3;
public $backoff = [60, 300, 900]; // 1min, 5min, 15min
public function failed(Throwable $e)
{
// Dit WORDT aangeroepen na alle retries uitgeput
$this->order->update(['processing_status' => 'failed_permanently']);
// Waarschuw iemand
Notification::send(
User::admins()->get(),
new OrderFailedNotification($this->order, $e)
);
}
}
Het grotere plaatje
Queue monitoring gaat niet alleen over het kijken naar Horizon. Het gaat om het hebben van meerdere verificatielagen:
- Applicatie-niveau tracking: Volg job status in je database
- Watchdog processen: Zoek actief naar vastgelopen of verweesde taken
- Business metrics: Monitor uitkomsten, niet alleen job aantallen
- Alerting: Word genotificeerd wanneer dingen misgaan, niet wanneer je het dashboard checkt
Conclusie
De queue is een tool, geen garantie. Vertrouw het systeem om zijn werk te doen, maar verifieer dat je business logic daadwerkelijk succesvol is afgerond. De paar minuten die je besteedt aan het implementeren van goede tracking besparen je uren debuggen van stille fouten.
Hulp nodig met queue architectuur of het debuggen van stille fouten? Laten we praten.
El problema: fallos silenciosos
El mes pasado, estaba depurando el sistema de procesamiento de pedidos de un cliente. Tenían una configuración clásica: el usuario hace un pedido, se despacha un job para procesar el pago y enviar la confirmación. Workers de cola ejecutándose vía Supervisor. Dashboard de Horizon mostrando todo verde.
Excepto que los clientes no recibían sus correos de confirmación. Algunos pedidos no se procesaban en absoluto. Y el monitoreo decía que todo estaba bien.
Esto es lo que estaba pasando: sus jobs estaban fallando, pero no de una manera que Laravel detectaría.
Cómo los jobs pueden fallar silenciosamente
El sistema de colas de Laravel es robusto, pero tiene algunas suposiciones incorporadas que pueden morderte:
1. Muertes por timeout
Cuando un job excede su timeout, el proceso worker lo mata. Pero dependiendo de tu configuración, esto podría no activar el método failed() en tu job:
// Este job será terminado después de 60 segundos
class ProcessOrder implements ShouldQueue
{
public $timeout = 60;
public function handle()
{
// Si esto toma > 60 segundos, el job muere
// Pero failed() podría nunca ser llamado
$this->processPayment();
}
public function failed(Throwable $e)
{
// ¡Esto podría no ser llamado en timeout!
Log::error('Pedido fallido', ['order' => $this->order->id]);
}
}
2. Muertes por límite de memoria
El mismo problema con la memoria. Si tu worker alcanza el límite de memoria, PHP muere. Sin excepción, sin handler de fallo, nada.
3. La mentira del "procesado"
Horizon y otras herramientas de monitoreo típicamente rastrean:
- Pending: Jobs en la cola
- Completed: Jobs que terminaron sin lanzar una excepción
- Failed: Jobs que lanzaron una excepción y excedieron los intentos de reintento
Pero "completed" no significa "exitoso". Un job puede completarse con trabajo parcial hecho, con errores silenciosos registrados, o con fallos de lógica de negocio que no lanzan excepciones.
La solución: confía pero verifica
1. Implementa verificación a nivel de job
No confíes en el sistema de colas para decirte si un job tuvo éxito. Rastréalo tú mismo:
class ProcessOrder implements ShouldQueue
{
public function handle()
{
$order = $this->order;
// Marcar como procesando
$order->update(['processing_status' => 'processing']);
try {
$this->processPayment();
$this->sendConfirmation();
// Marcar como completado
$order->update([
'processing_status' => 'completed',
'processed_at' => now(),
]);
} catch (Throwable $e) {
$order->update(['processing_status' => 'failed']);
throw $e;
}
}
}
2. Agrega una consulta watchdog
Ejecuta un comando programado que busque jobs huérfanos:
// En tu scheduler
$schedule->command('orders:check-stuck')->everyFiveMinutes();
// El comando
$stuckOrders = Order::where('processing_status', 'processing')
->where('updated_at', '<', now()->subMinutes(10))
->get();
foreach ($stuckOrders as $order) {
Log::warning('Pedido atascado detectado', ['order' => $order->id]);
// Alertar, reintentar, o intervención manual
}
3. Establece límites de reintento sensatos
No dejes que los jobs se reintenten para siempre. Establece límites explícitos y maneja el caso de fallo:
class ProcessOrder implements ShouldQueue
{
public $tries = 3;
public $backoff = [60, 300, 900]; // 1min, 5min, 15min
public function failed(Throwable $e)
{
// Esto SÍ será llamado después de agotar todos los reintentos
$this->order->update(['processing_status' => 'failed_permanently']);
// Notificar a alguien
Notification::send(
User::admins()->get(),
new OrderFailedNotification($this->order, $e)
);
}
}
El panorama general
El monitoreo de colas no se trata solo de ver Horizon. Se trata de tener múltiples capas de verificación:
- Rastreo a nivel de aplicación: Rastrea el estado del job en tu base de datos
- Procesos watchdog: Busca activamente trabajo atascado o huérfano
- Métricas de negocio: Monitorea resultados, no solo conteos de jobs
- Alertas: Recibe notificaciones cuando las cosas salen mal, no cuando revises el dashboard
Conclusión
La cola es una herramienta, no una garantía. Confía en que el sistema haga su trabajo, pero verifica que tu lógica de negocio realmente se completó exitosamente. Los pocos minutos invertidos en implementar un rastreo adecuado te ahorrarán horas depurando fallos silenciosos.
¿Necesitas ayuda con arquitectura de colas o depuración de fallos silenciosos? Hablemos.