Jobs, Queues y Workers en Laravel — Guía completa
Hay operaciones en tu aplicación que son lentas: enviar emails, generar PDFs, procesar imágenes, llamar a APIs externas. Si las ejecutas en el hilo principal de una request HTTP, el usuario tiene que esperar. La solución son las colas (queues) y los jobs asíncronos.
¿Por qué usar colas?
El escenario clásico:
// Sin colas: el usuario espera 5 segundos hasta que el email se envía
public function register(Request $request)
{
$user = User::create($request->validated());
Mail::to($user)->send(new WelcomeMail($user)); // 2-5 segundos
return redirect('/dashboard'); // El usuario espera todo esto
}
// Con colas: el usuario recibe respuesta inmediata
public function register(Request $request)
{
$user = User::create($request->validated());
SendWelcomeMail::dispatch($user); // Se pone en cola, no bloquea
return redirect('/dashboard'); // Respuesta inmediata
}
// El email se envía en segundo plano por el worker
Crear tu primer Job
php artisan make:job SendWelcomeMail
<?php
namespace App\Jobs;
use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendWelcomeMail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Número de intentos si el job falla
public int $tries = 3;
// Timeout en segundos
public int $timeout = 60;
// Backoff entre reintentos
public int $backoff = 30;
public function __construct(
private User $user // SerializesModels maneja la serialización
) {}
/**
* El método handle() es donde va la lógica del job
*/
public function handle(): void
{
Mail::to($this->user)->send(new WelcomeMail($this->user));
}
/**
* Se llama cuando el job agota todos sus intentos
*/
public function failed(\Throwable $exception): void
{
\Log::error("No se pudo enviar email de bienvenida a {$this->user->email}", [
'error' => $exception->getMessage(),
]);
// Notificar al equipo, actualizar estado, etc.
}
}
Por qué implements ShouldQueue
La interfaz ShouldQueue le dice a Laravel que este job debe procesarse en cola. Sin ella, dispatch() lo ejecutaría de forma síncrona (como si usaras dispatchSync()).
Configurar el driver de cola
En .env:
# Síncrono: no usa cola real, ejecuta el job inmediatamente (para desarrollo)
QUEUE_CONNECTION=sync
# Base de datos: guarda jobs en la tabla 'jobs' (fácil de configurar)
QUEUE_CONNECTION=database
# Redis: alta performance, recomendado para producción
QUEUE_CONNECTION=redis
# Amazon SQS: para aplicaciones en AWS
QUEUE_CONNECTION=sqs
Configurar el driver database
# Crear la tabla de jobs
php artisan queue:table
php artisan migrate
Esto crea la tabla jobs donde se guardan los jobs pendientes.
Despachar jobs
// Despachar inmediatamente a la cola por defecto
SendWelcomeMail::dispatch($user);
// Con delay: procesar después de 10 minutos
SendWelcomeMail::dispatch($user)->delay(now()->addMinutes(10));
// En una cola específica (debes tener workers escuchando esa cola)
SendWelcomeMail::dispatch($user)->onQueue('emails');
// Con una conexión específica
SendWelcomeMail::dispatch($user)->onConnection('redis');
// Despacho condicional
SendWelcomeMail::dispatchIf($user->wants_emails, $user);
// Forma alternativa con helper dispatch()
dispatch(new SendWelcomeMail($user));
// Síncrono (ignorar la cola, ejecutar ahora)
SendWelcomeMail::dispatchSync($user);
Encadenar jobs
// Los jobs se ejecutan en orden, uno después del otro
Bus::chain([
new CreateThumbnail($video),
new TranscodeVideo($video),
new NotifyUploadComplete($video),
])->dispatch();
Batch: grupos de jobs
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
$batch = Bus::batch([
new ImportUserJob($chunk1),
new ImportUserJob($chunk2),
new ImportUserJob($chunk3),
])->then(function (Batch $batch) {
// Todos los jobs completados
\Log::info("Importación completa: {$batch->processedJobs()} usuarios");
})->catch(function (Batch $batch, \Throwable $e) {
// Al menos un job falló
\Log::error("Error en importación masiva: " . $e->getMessage());
})->finally(function (Batch $batch) {
// Siempre se ejecuta al final
})->dispatch();
// Ver el progreso del batch
$progress = $batch->progress(); // 0-100
Ejecutar el worker
El worker es el proceso que escucha la cola y ejecuta los jobs:
# Escuchar la cola por defecto
php artisan queue:work
# Escuchar una cola específica
php artisan queue:work --queue=emails
# Escuchar múltiples colas en orden de prioridad
php artisan queue:work --queue=high,default,low
# Procesar solo un job y parar (útil para debugging)
php artisan queue:work --once
# Con conexión específica
php artisan queue:work redis
# Con opciones de control
php artisan queue:work \
--tries=3 \
--timeout=60 \
--sleep=3 \ # Segundos de espera cuando la cola está vacía
--verbose
Diferencia entre queue:work y queue:listen
# queue:work: recomendado para producción
# Carga el código una vez, muy eficiente
# PERO: necesitas reiniciarlo cuando despliegas código nuevo
php artisan queue:work
# queue:listen: para desarrollo
# Recarga el código en cada job
# Más lento pero útil cuando estás cambiando código frecuentemente
php artisan queue:listen
Reiniciar workers en producción
Después de cada despliegue, debes reiniciar los workers para que carguen el código nuevo:
# Señaliza a los workers que terminen el job actual y paren
php artisan queue:restart
# Los workers se reinician automáticamente si usas Supervisor
Supervisor: mantener los workers corriendo
En producción, los workers deben sobrevivir reinicios del servidor. Para eso se usa supervisor:
; /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/mi-app/artisan queue:work redis --sleep=3 --tries=3 --timeout=60
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=4 ; Número de workers en paralelo
redirect_stderr=true
stdout_logfile=/var/www/html/mi-app/storage/logs/worker.log
stopwaitsecs=3600
# Comandos de supervisor
supervisorctl reread
supervisorctl update
supervisorctl start laravel-worker:*
supervisorctl restart laravel-worker:*
Horizon: panel de control para Redis
Si usas Redis como driver, Laravel Horizon añade un dashboard web para monitorizar las colas:
composer require laravel/horizon
php artisan horizon:install
php artisan migrate
// config/horizon.php: configurar los workers
'environments' => [
'production' => [
'supervisor-1' => [
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
],
# Iniciar Horizon (reemplaza a queue:work cuando lo usas)
php artisan horizon
# Acceder al dashboard
# http://tu-app.com/horizon
Horizon te muestra: jobs procesados por minuto, jobs fallidos, tiempo de respuesta de cada cola, y gráficas históricas.
Prioridad de colas
Puedes tener múltiples colas con diferentes prioridades:
// Jobs de alta prioridad
ProcessPayment::dispatch($order)->onQueue('high');
// Jobs normales
SendNewsletter::dispatch($campaign)->onQueue('default');
// Jobs de baja prioridad (reportes, exportaciones)
GenerateMonthlyReport::dispatch()->onQueue('low');
// El worker procesa 'high' antes que 'default', y este antes que 'low'
// php artisan queue:work --queue=high,default,low
Conclusión
Las colas en Laravel son el mecanismo para mover operaciones lentas fuera del ciclo de vida de una request. El patrón es simple: crear un job con make:job, implementar la lógica en handle(), despachar con dispatch(), y mantener un worker corriendo con Supervisor. Para producción con Redis, usa Laravel Horizon para tener visibilidad completa de lo que pasa en tus colas.