Saltar al contenido principal

Qué es una cola

En una aplicación web hay operaciones que tardan varios segundos: enviar correos, redimensionar imágenes, llamar a APIs externas… Si las haces de forma síncrona durante la petición HTTP, el usuario tiene que esperar a que terminen para recibir la respuesta. Con las colas de Laravel puedes ejecutar esas operaciones en segundo plano de forma asíncrona. La petición responde inmediatamente y el trabajo real lo lleva a cabo un proceso worker aparte.
Las colas admiten varios backends (base de datos, Redis, Amazon SQS, etc.). En desarrollo puedes usar el driver sync para ejecutar los jobs al instante, sin cola.

Configuración de la cola

config/queue.php

Toda la configuración se centraliza en config/queue.php. La variable QUEUE_CONNECTION selecciona el driver.
// config/queue.php
'default' => env('QUEUE_CONNECTION', 'database'),

Configuración en .env

# Selección del driver
QUEUE_CONNECTION=database

# Ejemplo con Redis
# QUEUE_CONNECTION=redis
# REDIS_HOST=127.0.0.1
# REDIS_PORT=6379

Preparar el driver de base de datos

Si usas database, necesitas una tabla para almacenar los jobs. Los proyectos nuevos con Laravel 11+ incluyen la migración; si no, créala con:
php artisan make:queue-table
php artisan migrate

Preparar el driver de Redis

Añade la configuración de Redis en config/database.php e instala el driver con Composer.
composer require predis/predis

Overflow storage de SQS

Amazon SQS limita el tamaño de los mensajes. Si tus jobs generan payloads grandes, puedes configurar el excedente para guardarlo en la caché y enviar solo un puntero a SQS.
'sqs' => [
    // ...
    'overflow' => [
        'enabled' => env('SQS_OVERFLOW_ENABLED', false),
        'store' => env('SQS_OVERFLOW_STORE'),
        'always' => false,
        'delete_after_processing' => true,
        'flush_on_clear' => env('SQS_OVERFLOW_FLUSH_ON_CLEAR', false),
    ],
],
  • Con enabled, los payloads de más de 1 MB se guardan en el almacén de caché indicado.
  • Con always: true, se guardan siempre, sea cual sea su tamaño.
  • delete_after_processing elimina el payload al finalizar con éxito (por defecto true).
  • flush_on_clear: true vacía el store de overflow al ejecutar queue:clear. Como no queremos borrar el resto de la caché, úsalo con un store dedicado.

Crear la clase de job

Comando make:job

Genera el esqueleto con make:job.
php artisan make:job SendWelcomeEmail
Se crea app/Jobs/SendWelcomeEmail.php.

Estructura de la clase

<?php

namespace App\Jobs;

use App\Models\User;
use App\Mail\WelcomeMail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail implements ShouldQueue
{
    use Queueable;

    /**
     * Crea una nueva instancia del job
     */
    public function __construct(
        public User $user,
    ) {}

    /**
     * Ejecuta el job
     */
    public function handle(): void
    {
        Mail::to($this->user->email)->send(new WelcomeMail($this->user));
    }
}
Al implementar ShouldQueue, indicamos a Laravel que este job debe procesarse en cola. El trait Queueable aporta los métodos necesarios para operar sobre la cola.
Cuando pasas un modelo Eloquent al constructor, Laravel serializa solamente el ID. Al ejecutarse el job se recupera el modelo actualizado de la base de datos, con lo que la payload de la cola queda ligera.

Despachar un job

dispatch()

Desde un controlador o servicio, envía el job a la cola con dispatch().
use App\Jobs\SendWelcomeEmail;

// En una ruta o controlador
public function register(Request $request): RedirectResponse
{
    $user = User::create($request->validated());

    // Encolar el job
    SendWelcomeEmail::dispatch($user);

    return redirect('/dashboard');
}

Despacho retrasado

Con delay() retrasas la ejecución.
// Se ejecuta 5 minutos después
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));

dispatchAfterResponse()

Ejecuta el job inmediatamente después de devolver la respuesta HTTP al usuario. Funciona incluso con el driver sync, así que es útil para casos ligeros que no necesitan worker.
SendWelcomeEmail::dispatchAfterResponse($user);

Despachar a una cola concreta

SendWelcomeEmail::dispatch($user)->onQueue('emails');

Queue Routing

Para dirigir por defecto un job a una conexión o cola concreta, usa Queue::route() en el boot() de un service provider. Así centralizas la configuración en vez de repetir onQueue() / onConnection() en cada job.
use App\Concerns\RequiresVideo;
use App\Jobs\ProcessPodcast;
use App\Jobs\ProcessVideo;
use Illuminate\Support\Facades\Queue;

public function boot(): void
{
    Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts');
    Queue::route(RequiresVideo::class, queue: 'video');
}
También puedes usar interfaces, traits o clases padre; se aplica a todos los jobs que las implementen, usen o hereden. Para agrupar varios jobs, pasa un array.
Queue::route([
    ProcessPodcast::class => ['podcasts', 'redis'], // queue y connection
    ProcessVideo::class => 'videos',                // solo queue (usa la conexión por defecto)
]);
El propio job puede sobreescribir el Queue Routing mediante onQueue() u onConnection().

Ejecución síncrona (para tests y desarrollo)

Con dispatchSync() el job se ejecuta al momento, sin pasar por la cola.
SendWelcomeEmail::dispatchSync($user);

Despacho masivo (bulk)

Cuando necesitas despachar muchos jobs independientes, puedes usar Bus::bulk(). Es ideal si no necesitas seguimiento ni callbacks (como un batch). Bus::bulk() agrupa los jobs por conexión y cola configuradas y los empuja de golpe a la cola, con más eficiencia.
use App\Jobs\ProcessUser;
use Illuminate\Support\Facades\Bus;

Bus::bulk(
    $users->map(fn ($user) => new ProcessUser($user))
);
Bus::bulk() envía los jobs agrupados por lotes a la cola. A diferencia del batching (Bus::batch()), no ofrece progreso ni callbacks al terminar. Es la opción sencilla para enviar muchos jobs independientes de una tacada.

Procesar los jobs

Comando queue:work

Arranca un worker para procesar los jobs.
php artisan queue:work
Puedes indicar el driver o la cola concreta.
# Solo la cola "emails" de Redis
php artisan queue:work redis --queue=emails

# Usa el driver database
php artisan queue:work database
queue:work se mantiene en ejecución continua. Cuando cambies el código, usa queue:restart para reiniciarlo. En producción se gestiona típicamente con Supervisor u otro process manager.

Opciones para monitorizar el worker

Puedes ajustar el comportamiento del worker con estas opciones.
php artisan queue:work --tries=3 --timeout=60 --sleep=3
OpciónDescripción
--tries=NMáximo de reintentos; al superarlo se marca como fallido
--timeout=NMáximo de segundos por job; si se supera, se aborta el worker
--sleep=NSegundos entre sondeos cuando la cola está vacía (por defecto 3)
--max-jobs=NTermina tras procesar N jobs
--max-time=NTermina tras N segundos
--queue=A,BProcesa colas con prioridad (A antes que B)

Configuración de reintentos en la propia clase

En muchos casos es más práctico definir la configuración en el propio job que como opciones de línea de comandos.
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Timeout;

#[Tries(3)]
#[Timeout(60)]
class SendWelcomeEmail implements ShouldQueue
{
    use Queueable;

    // ...
}

Liberar un job (middleware Release)

Si bajo cierta condición quieres devolver el job a la cola sin ejecutarlo, utiliza el middleware Release.
use Illuminate\Queue\Middleware\Release;

/**
 * Devuelve los middleware por los que pasa el job
 */
public function middleware(): array
{
    return [
        // Si $condition es true, se libera y se reintenta a los 60 s
        Release::when($this->order->isPending(), releaseAfter: 60),
    ];
}
Release::unless() libera cuando la condición es false.
return [
    // Libera a los 60 s si el pedido no está pagado
    Release::unless($this->order->isPaid(), releaseAfter: 60),
];
También puedes usar una closure para condiciones más complejas.
return [
    Release::when(function (): bool {
        return ! $this->order->isPaid();
    }, releaseAfter: 60),
];
Al liberar un job se incrementa igualmente el contador de intentos. Ajusta bien #[Tries] o la propiedad $tries.

Jobs fallidos

Preparar la tabla failed_jobs

Cuando se supera el número de reintentos, los jobs se guardan en failed_jobs. Si no tienes la tabla, créala:
php artisan make:queue-failed-table
php artisan migrate

Limpieza al fallar

Definiendo failed() en el job, puedes ejecutar código cuando falle definitivamente.
use Throwable;

public function failed(?Throwable $exception): void
{
    // Notificar al administrador por Slack, por ejemplo
    // Notification::route('slack', config('app.slack_webhook'))
    //     ->notify(new JobFailedNotification($this, $exception));
}

Detener reintentos por tipo de excepción

Para excepciones que no se resolverán reintentando, indícalas en el withExceptions() de bootstrap/app.php con dontRetry.
use App\Exceptions\InvalidPodcastSourceException;
use Illuminate\Foundation\Configuration\Exceptions;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontRetry([
        InvalidPodcastSourceException::class,
    ]);
})
Para un control más fino, dontRetryWhen recibe una closure. Si devuelve true, el job se marca como fallido de inmediato.
use App\Exceptions\PodcastProcessingException;
use Illuminate\Foundation\Configuration\Exceptions;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontRetryWhen(function (PodcastProcessingException $e) {
        return $e->reason() === 'Subscription expired';
    });
})
Con excepciones cuyo resultado no cambia al reintentar (errores de validación, fallos de pago por suscripción caducada…), esta técnica evita reintentos inútiles.

Listar jobs fallidos

php artisan queue:failed

Reintentar jobs fallidos

# Un job concreto por UUID
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece

# Todos los fallidos
php artisan queue:retry all

Eliminar jobs fallidos

# Uno concreto
php artisan queue:forget ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece

# Todos
php artisan queue:flush

Drivers de cola habituales

Driver database

Es el más sencillo, se usa sin middleware adicional. Los jobs se guardan en la tabla jobs y el worker hace polling.
  • Ventaja: fácil de configurar, aprovecha la BD existente.
  • Desventaja: carga la base de datos; no es ideal para volúmenes muy altos.
QUEUE_CONNECTION=database

Driver redis

El más habitual en producción por su velocidad. Al trabajar en memoria supera con creces la BD en throughput.
  • Ventajas: rapidez, escalabilidad.
  • Desventaja: requiere un servidor Redis.
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
En producción con Redis considera adoptar Laravel Horizon. Ofrece un panel bonito y en tiempo real del estado de los jobs.

Operación en producción con Supervisor

En producción es imprescindible reiniciar queue:work automáticamente si cae. En Linux, lo habitual es usar Supervisor.
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/your-app/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/your-app/storage/logs/worker.log
stopwaitsecs=3600
numprocs=2 arranca 2 workers en paralelo. Recarga Supervisor tras editar la configuración.
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*

Ejemplo práctico: enviar correos desde la cola

1

Crear la clase de job

php artisan make:job SendOrderConfirmation
2

Implementar la lógica

<?php

namespace App\Jobs;

use App\Models\Order;
use App\Mail\OrderConfirmed;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Support\Facades\Mail;

#[Tries(3)]
#[Timeout(30)]
class SendOrderConfirmation implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public Order $order,
    ) {}

    public function handle(): void
    {
        Mail::to($this->order->user->email)
            ->send(new OrderConfirmed($this->order));
    }
}
3

Despachar desde el controlador

use App\Jobs\SendOrderConfirmation;

public function store(Request $request): RedirectResponse
{
    $order = Order::create($request->validated());

    SendOrderConfirmation::dispatch($order);

    return redirect()->route('orders.show', $order)
        ->with('success', 'Pedido registrado.');
}
4

Arrancar el worker

php artisan queue:work --tries=3 --timeout=30

Resumen

  • Envío de correos y SMS.
  • Redimensionado o conversión de imágenes y vídeos.
  • Peticiones a APIs externas.
  • Generación de informes y exportaciones CSV.
  • Envío de webhooks.
Poniendo QUEUE_CONNECTION=sync en .env, los jobs se ejecutan al instante sin cola. Sin necesidad de arrancar un worker, es muy cómodo durante el desarrollo.
QUEUE_CONNECTION=sync
# Iniciar el worker
php artisan queue:work

# Reiniciar el worker (tras un deploy)
php artisan queue:restart

# Listar jobs fallidos
php artisan queue:failed

# Reintentar todos
php artisan queue:retry all

# Eliminar todos
php artisan queue:flush
Última modificación el 13 de julio de 2026