Colas y Jobs
Las colas permiten diferir tareas pesadas para ejecutarlas en segundo plano: enviar emails, procesar imágenes, generar reportes o llamar APIs externas. El usuario no tiene que esperar y tu aplicación responde más rápido.
¿Por qué usar colas?
Imagina que cuando un usuario sube una imagen quieres generar varias miniaturas. Sin colas, el usuario espera hasta que termine el proceso:
<?php
// Sin colas: el usuario espera mientras se procesan las imágenes
public function store(Request $request): RedirectResponse
{
$path = $request->file('image')->store('images');
// Esto puede tardar varios segundos...
Image::make(storage_path("app/{$path}"))
->resize(800, 600)->save(storage_path('app/thumbnails/large/' . basename($path)));
Image::make(storage_path("app/{$path}"))
->resize(400, 300)->save(storage_path('app/thumbnails/medium/' . basename($path)));
Image::make(storage_path("app/{$path}"))
->resize(150, 150)->save(storage_path('app/thumbnails/small/' . basename($path)));
return redirect()->route('images.index');
}
Con colas, el usuario recibe respuesta inmediata y las miniaturas se generan en segundo plano:
<?php
// Con colas: respuesta inmediata, procesamiento en segundo plano
public function store(Request $request): RedirectResponse
{
$path = $request->file('image')->store('images');
ProcessImageThumbnails::dispatch($path);
return redirect()->route('images.index')
->with('success', 'Imagen subida. Las miniaturas se generarán en breve.');
}
Configurar el driver de colas
Laravel soporta varios drivers de colas. El más
común en producción es database (usa
tu base de datos) o redis (más rápido,
requiere Redis). Para desarrollo puedes usar
sync (ejecuta inmediatamente) o
database.
Configura el driver en .env:
QUEUE_CONNECTION=database
Si usas el driver database, necesitas
crear la tabla de jobs:
php artisan make:queue-table
php artisan migrate
Crear un Job
Un job es una clase que encapsula una tarea que se ejecutará en segundo plano:
php artisan make:job ProcessImageThumbnails
<?php
declare(strict_types=1);
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
class ProcessImageThumbnails implements ShouldQueue
{
use Queueable;
public function __construct(
public string $imagePath
) {}
public function handle(): void
{
Log::info("Procesando miniaturas para: {$this->imagePath}");
// Aquí iría la lógica de procesamiento de imágenes
// Por ahora solo simulamos el proceso
}
}
La interfaz ShouldQueue indica que
el job debe ejecutarse en una cola. El trait
Queueable proporciona métodos útiles
como onQueue() y delay().
Despachar un Job
Para enviar un job a la cola, usa el método
dispatch():
<?php
use App\Jobs\ProcessImageThumbnails;
// Despachar inmediatamente a la cola
ProcessImageThumbnails::dispatch($imagePath);
// Despachar con retraso (ejecutar en 5 minutos)
ProcessImageThumbnails::dispatch($imagePath)->delay(now()->addMinutes(5));
// Despachar a una cola específica
ProcessImageThumbnails::dispatch($imagePath)->onQueue('images');
Ejecutar el worker
Los jobs en cola no se ejecutan solos. Necesitas un worker que procese la cola:
# Ejecutar el worker (mantén esto corriendo)
php artisan queue:work
# Procesar solo un job y terminar
php artisan queue:work --once
# Procesar una cola específica
php artisan queue:work --queue=images
# Con más información de depuración
php artisan queue:work -v
Importante: En producción, el worker debe ejecutarse como un servicio (supervisor, systemd) para que se reinicie automáticamente si falla.
Pasar datos al Job
Puedes pasar cualquier dato serializable al constructor del job. Los modelos Eloquent se serializan automáticamente por su ID:
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Order;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class SendOrderConfirmation implements ShouldQueue
{
use Queueable;
public function __construct(
public Order $order,
public User $user
) {}
public function handle(): void
{
// $this->order y $this->user se recuperan
// automáticamente de la base de datos
Mail::to($this->user)->send(new OrderConfirmationMail($this->order));
}
}
<?php
// Despachar con modelos
SendOrderConfirmation::dispatch($order, $user);
Reintentos y fallos
Puedes configurar cuántas veces se reintenta un job que falla:
<?php
declare(strict_types=1);
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class CallExternalApi implements ShouldQueue
{
use Queueable;
// Número máximo de intentos
public int $tries = 3;
// Segundos de espera entre intentos (backoff exponencial)
public array $backoff = [10, 60, 300];
// Tiempo máximo de ejecución en segundos
public int $timeout = 120;
public function handle(): void
{
// Si falla, se reintenta según la configuración
$response = Http::timeout(30)->get('https://api.example.com/data');
if ($response->failed()) {
throw new \Exception('API no disponible');
}
}
// Se ejecuta cuando el job falla definitivamente
public function failed(\Throwable $exception): void
{
Log::error('Job CallExternalApi falló', [
'error' => $exception->getMessage(),
]);
}
}
Jobs únicos
Puedes evitar que se encolen jobs duplicados
implementando ShouldBeUnique:
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class GenerateUserReport implements ShouldQueue, ShouldBeUnique
{
use Queueable;
public function __construct(
public User $user
) {}
// La clave de unicidad: solo un job por usuario
public function uniqueId(): string
{
return (string) $this->user->id;
}
// Tiempo que se mantiene el lock de unicidad (segundos)
public int $uniqueFor = 3600;
public function handle(): void
{
// Generar reporte...
}
}
Si intentas despachar un job mientras otro con
el mismo uniqueId está en cola o
ejecutándose, el nuevo job se descarta
silenciosamente.
Encadenar Jobs
Puedes ejecutar jobs en secuencia usando cadenas:
<?php
use Illuminate\Support\Facades\Bus;
Bus::chain([
new ProcessPodcast($podcast),
new OptimizePodcast($podcast),
new ReleasePodcast($podcast),
])->dispatch();
// Si algún job falla, los siguientes no se ejecutan
Batches de Jobs
Para procesar muchos jobs y saber cuándo terminan todos, usa batches:
# Crear tabla para batches
php artisan make:queue-batches-table
php artisan migrate
<?php
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
$users = User::all();
$jobs = $users->map(fn (User $user) => new SendNewsletterEmail($user));
$batch = Bus::batch($jobs)
->then(function (Batch $batch) {
Log::info('Todos los emails enviados correctamente');
})
->catch(function (Batch $batch, \Throwable $e) {
Log::error('Algún email falló: ' . $e->getMessage());
})
->finally(function (Batch $batch) {
Log::info('Batch completado');
})
->dispatch();
// Obtener el progreso
$batch->progress(); // 0-100
Jobs fallidos
Cuando un job falla después de todos los reintentos,
se guarda en la tabla failed_jobs:
# Ver jobs fallidos
php artisan queue:failed
# Reintentar un job fallido específico
php artisan queue:retry 5
# Reintentar todos los jobs fallidos
php artisan queue:retry all
# Eliminar un job fallido
php artisan queue:forget 5
# Eliminar todos los jobs fallidos
php artisan queue:flush
Resumen
- Las colas permiten ejecutar tareas en segundo plano sin hacer esperar al usuario
- Un job es una clase que implementa
ShouldQueue - Usa
dispatch()para enviar jobs a la cola - El worker (
queue:work) procesa los jobs encolados - Configura
$triesy$backoffpara manejar fallos - Implementa
ShouldBeUniquepara evitar jobs duplicados - Usa chains para jobs secuenciales y batches para procesar muchos jobs
Ejercicios
Ejercicio 1: Job básico de envío de email
Crea un job SendWelcomeEmail que reciba
un usuario y simule el envío de un email de bienvenida
usando Log::info. Configúralo para que
se reintente 3 veces si falla.
Ver solución
php artisan make:job SendWelcomeEmail
<?php
// app/Jobs/SendWelcomeEmail.php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
class SendWelcomeEmail implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public array $backoff = [10, 30, 60];
public function __construct(
public User $user
) {}
public function handle(): void
{
Log::info('Enviando email de bienvenida', [
'user_id' => $this->user->id,
'email' => $this->user->email,
'name' => $this->user->name,
]);
}
public function failed(\Throwable $exception): void
{
Log::error('Fallo al enviar email de bienvenida', [
'user_id' => $this->user->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
// Uso en el controlador
use App\Jobs\SendWelcomeEmail;
public function register(Request $request): RedirectResponse
{
$user = User::create($validated);
SendWelcomeEmail::dispatch($user);
return redirect()->route('dashboard');
}
Ejercicio 2: Job con retraso
Crea un job SendReminderEmail que se
ejecute 24 horas después de ser despachado. El job
debe recibir un pedido (Order) y registrar
en el log un recordatorio de que el pedido está
pendiente de pago.
Ver solución
php artisan make:job SendReminderEmail
<?php
// app/Jobs/SendReminderEmail.php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
class SendReminderEmail implements ShouldQueue
{
use Queueable;
public function __construct(
public Order $order
) {}
public function handle(): void
{
// Verificar si el pedido sigue pendiente de pago
if ($this->order->status !== 'pending') {
Log::info('Pedido ya no está pendiente, omitiendo recordatorio', [
'order_id' => $this->order->id,
'status' => $this->order->status,
]);
return;
}
Log::info('Enviando recordatorio de pago pendiente', [
'order_id' => $this->order->id,
'user_email' => $this->order->user->email,
'total' => $this->order->total,
]);
}
}
<?php
// Uso en el controlador
use App\Jobs\SendReminderEmail;
public function store(Request $request): RedirectResponse
{
$order = Order::create([
'user_id' => auth()->id(),
'total' => $request->total,
'status' => 'pending',
]);
// Programar recordatorio para 24 horas después
SendReminderEmail::dispatch($order)->delay(now()->addHours(24));
return redirect()->route('orders.show', $order);
}
Ejercicio 3: Job único por usuario
Crea un job GenerateMonthlyReport que
genere un reporte mensual para un usuario. Debe ser
único por usuario (no se pueden encolar dos reportes
para el mismo usuario) y el lock debe mantenerse
durante 1 hora.
Ver solución
php artisan make:job GenerateMonthlyReport
<?php
// app/Jobs/GenerateMonthlyReport.php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
class GenerateMonthlyReport implements ShouldQueue, ShouldBeUnique
{
use Queueable;
// Lock de unicidad: 1 hora
public int $uniqueFor = 3600;
public function __construct(
public User $user,
public string $month
) {}
// Clave de unicidad: solo un reporte por usuario
public function uniqueId(): string
{
return (string) $this->user->id;
}
public function handle(): void
{
Log::info('Generando reporte mensual', [
'user_id' => $this->user->id,
'month' => $this->month,
]);
// Simular generación del reporte (proceso largo)
sleep(5);
Log::info('Reporte mensual generado', [
'user_id' => $this->user->id,
'month' => $this->month,
]);
}
}
<?php
// Uso en el controlador
use App\Jobs\GenerateMonthlyReport;
public function generateReport(User $user): RedirectResponse
{
$month = now()->format('Y-m');
// Si ya hay un reporte en cola para este usuario,
// este dispatch se ignora silenciosamente
GenerateMonthlyReport::dispatch($user, $month);
return redirect()->back()
->with('success', 'Reporte en proceso de generación.');
}
¿Has encontrado un error o tienes una sugerencia para mejorar esta lección?
Escríbenos¿Quieres dominar Jobs y Queues a fondo?
Aprende procesamiento asíncrono avanzado, workers en producción, Laravel Horizon y más en nuestro curso especializado.
Ver curso de Jobs y Queues