Colas y jobs en Laravel 13: del primer dispatch a Horizon en producción
Un email de confirmación que tarda dos segundos dentro del request es la diferencia entre un cliente satisfecho y uno que abandona la compra. Las colas en Laravel 13 resuelven ese problema moviendo el trabajo pesado a segundo plano: esta guía te lleva del primer dispatch con el driver database hasta Horizon en producción.
Por qué necesitas colas en tu aplicación Laravel
El problema del trabajo síncrono dentro del request
Cada vez que tu controlador envía un email, genera una miniatura o llama a un webhook externo, la petición HTTP espera a que esa tarea termine antes de devolver la respuesta. Enviar un correo a través de SMTP puede añadir uno o dos segundos; generar una imagen o consultar una API de terceros, mucho más. El usuario percibe esa espera como lentitud y, si el servicio externo tarda o falla, tu aplicación puede devolver un error 500 por algo que no era responsabilidad directa de la página.
Qué resuelve una cola: disponibilidad, reintentos y escala
Una cola desacopla la petición del trabajo pesado: el controlador encola la tarea, responde al instante y un proceso worker la ejecuta en segundo plano. Eso mejora la disponibilidad, porque el request ya no depende del servicio externo; añade reintentos automáticos, porque un job que falla se puede volver a intentar; y facilita la escala, porque puedes lanzar tantos workers como necesites sin tocar el código de la aplicación.
Cómo funcionan las colas en Laravel 13
Drivers: database, Redis, SQS, Beanstalkd y sync
Laravel 13 incluye en su configuración de colas conexiones para varios backends: database guarda los jobs en una tabla de MySQL o SQLite, Redis los almacena en memoria para máxima velocidad, Amazon SQS ofrece una cola gestionada en la nube, Beanstalkd es un servidor de colas ligero, y el driver sync ejecuta los jobs inmediatamente en el mismo proceso, algo útil para desarrollo y testing. La conexión activa se define con la variable QUEUE_CONNECTION en el archivo .env.
La tabla jobs y el flujo dispatch → worker
Cuando despachas un job, Laravel serializa la clase y sus datos en la tabla jobs con un estado pendiente. El worker, un proceso PHP ejecutado con el comando artisan queue:work, lee esa tabla, reserva el primer job disponible y ejecuta su método handle. Si termina bien, lo elimina de la cola; si lanza una excepción, lo libera para reintentarlo o lo mueve a la tabla de fallidos cuando se agotan los intentos. En un proyecto nuevo de Laravel 13, la migración de la tabla jobs viene incluida por defecto; si tu aplicación es antigua y no la tiene, la creas con php artisan make:queue-table.
Tu primer job: procesar un pedido
php artisan make:job y la interfaz ShouldQueue
Vamos a construir el ejemplo clásico: procesar un pedido recién pagado. El primer paso es generar la clase del job:
php artisan make:job ProcessOrderEl comando crea una clase en App\Jobs. Para que Laravel la trate como trabajo asíncrono, debe implementar la interfaz ShouldQueue. El job recibe el modelo del pedido y en su método handle ejecuta los pasos pesados:
<?php
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessOrder implements ShouldQueue
{
use Queueable;
public function __construct(
public Order $order,
) {}
public function handle(): void
{
// Enviar email de confirmación
Mail::to($this->order->email)->send(new OrderConfirmation($this->order));
// Generar la factura en PDF
$pdf = InvoicePdf::generate($this->order);
// Notificar al proveedor por webhook
Http::post(config('services.warehouse.webhook'), $this->order->toArray());
}
}Encolar con dispatch() y retrasar con delay
Desde el controlador, encolar el job es una sola línea: ProcessOrder::dispatch($order). La petición devuelve la respuesta inmediatamente y el worker procesa el pedido en segundo plano. Si quieres que el trabajo no arranque hasta dentro de unos minutos, por ejemplo para dar margen a la pasarela de pago, añades delay:
ProcessOrder::dispatch($order)->delay(now()->addMinutes(5));Ejecutar el worker en desarrollo: queue:work
Para que los jobs se procesen necesitas un worker corriendo. En desarrollo basta con: php artisan queue:work. El proceso se queda en primer plano leyendo la cola y ejecutando los jobs conforme llegan. En lugar de abrir varias terminales, puedes ejecutarlo en segundo plano o usar el driver sync en local para que los jobs corran al instante y no tengas que mantener ningún proceso extra.
De database a Redis: cuándo cambiar de driver
Colas Redis-free con el driver database
Muchas aplicaciones Laravel 13 funcionan perfectamente sin Redis: el driver database guarda los jobs en la tabla correspondiente y, con un worker por detrás, el rendimiento es más que suficiente para decenas de miles de jobs al día. Es la opción más sencilla para un servidor pequeño, porque no añade ningún servicio extra que instalar ni vigilar.
Por qué Redis gana con volumen
Cuando el número de jobs crece, Redis aporta dos ventajas: la escritura y lectura son mucho más rápidas al vivir en memoria, y permite el panel de Horizon, que solo funciona con conexiones Redis. El cambio es mínimo: instalar el cliente (predis o phpredis) y poner QUEUE_CONNECTION=redis en el entorno. El resto del código no cambia.
Reintentos, timeouts y jobs fallidos
tries, backoff y maxExceptions
Un job puede fallar por un servicio externo caído o por un error transitorio. Laravel reintenta automáticamente según las propiedades de la clase: tries define el número máximo de intentos, backoff los segundos de espera entre intentos (admite un array para esperas progresivas) y timeout limita los segundos que el worker dedica al job antes de matarlo:
public $tries = 3;
public $backoff = [10, 60, 300];
public $timeout = 120;
public $maxExceptions = 2;Con esa configuración, un job que falle se reintentará a los 10 segundos, luego a los 60 y por último a los 300. maxExceptions permite que un job con varios fallos no consuma todos los intentos si solo unos pocos fueron excepciones puntuales.
La tabla failed_jobs y queue:retry
Cuando un job agota sus intentos, Laravel lo mueve a la tabla failed_jobs y registra el motivo. Ahí no se queda para siempre: puedes ver los fallos con php artisan queue:failed, reintentar todos con php artisan queue:retry all y borrar uno concreto con queue:forget. Es el circuito de recuperación manual que evita perder trabajo de forma silenciosa.
Batching y cadenas de jobs
Bus::batch para procesar lotes
Cuando una operación se divide en varias tareas independientes, puedes agruparlas en un lote con Bus::batch. Laravel crea un registro en la tabla job_batches y permite ejecutar callbacks when el lote termina, cuando falla o cuando se cancela:
use Illuminate\Support\Facades\Bus;
Bus::batch([
new ProcessOrder($order),
new SendInvoiceEmail($order),
new NotifyWarehouse($order),
])->then(function () use ($order) {
$order->markAsProcessed();
})->dispatch();Encadenar jobs con ->chain()
Si las tareas deben ejecutarse en orden estricto, una detrás de otra, usa cadenas. Encadenar jobs garantiza que el siguiente no arranque hasta que el anterior termine correctamente; si uno falla, el resto de la cadena no se ejecuta:
ProcessOrder::dispatch($order)->chain([
new SendInvoiceEmail($order),
new NotifyWarehouse($order),
]);Jobs únicos y rate limiting
Hay operaciones que no deben duplicarse: reindexar un modelo concreto, sincronizar una entidad con un servicio externo o regenerar una cache. Para eso existe la interfaz ShouldBeUnique, que impide encolar un segundo job idéntico mientras el primero siga pendiente o en proceso. Y si tu job llama a una API de terceros con límite de peticiones, el middleware RateLimited de Laravel pausa el procesamiento cuando alcanzas el límite en lugar de fallar ruidosamente.
Horizon: el panel de colas Redis en producción
Supervisores declarativos y métricas en tiempo real
Horizon es el panel de administración oficial de Laravel para colas Redis. En lugar de lanzar workers a mano, defines supervisores en su archivo de configuración: qué colas atienden, cuántos procesos mínimo y máximo, y cómo repartir el trabajo. El dashboard muestra en tiempo real los jobs en cola, en proceso y completados, con métricas de rendimiento que te dicen si necesitas más workers antes de que las colas se acumulen.
Gestión de fallos desde el dashboard
Los jobs fallidos aparecen en Horizon con su excepción completa y el payload que los provocó. Desde el panel puedes reintentarlos o eliminarlos sin tocar la terminal, lo que convierte el trabajo de recuperación en una tarea de un clic. Eso sí: Horizon requiere Redis, así que es el empujón definitivo para dejar el driver database atrás en producción.
Workers en producción: supervisord y Octane
En producción, un worker no puede depender de una terminal abierta. La forma estándar es gestionarlo con supervisord, que lo mantiene vivo y lo reinicia si se cae. Un programa típico lanza varios procesos de queue:work con reintentos configurados:
[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
numprocs=4
redirect_stderr=trueSi usas Laravel Octane, la versión 13 incluye mejoras en el ciclo de vida de los workers, como la nueva razón WorkerStopReason que informa con precisión por qué se detuvo un proceso. Da igual el camino: la idea es que los workers sean procesos de larga vida supervisados, no comandos lanzados a mano.
Conclusión
Las colas en Laravel 13 son la herramienta que separa una aplicación que responde al instante de una que se bloquea con cada email o webhook. Empieza con el driver database y un solo job, domina reintentos y batching, y cuando el volumen lo pida, salta a Redis con Horizon para vigilar todo desde un panel. Si quieres seguir profundizando en Laravel 13, no te pierdas el resto de guías del blog sobre el framework.