Laravel Queues en 2026: escalado horizontal de workers con Redis y Kubernetes
Introducción: cuando el monolito deja de dar abasto
Las colas de tareas son de las primeras cosas que fallan cuando aumenta la carga. Con tráfico moderado, un solo proceso php artisan queue:work en el servidor es suficiente. Pero cuando el número de trabajos en cola crece más rápido de lo que el worker los procesa, aparecen los problemas clásicos: retrasos en la entrega de emails, exportaciones bloqueadas, notificaciones acumuladas y colas desbordadas.
En una arquitectura monolítica, el escalado horizontal de workers es un dolor de cabeza. Hay que lanzar nuevos procesos manualmente, controlar su estado y balancear la carga. En 2026, el stack Laravel + Redis + Kubernetes resuelve este problema con elegancia: declaras el estado deseado y la infraestructura se adapta sola a la carga.
Este artículo es una guía práctica para desarrolladores PHP de nivel medio/senior que quieren construir un sistema fiable y escalable de procesamiento de tareas en segundo plano basado en Laravel Queues, Redis Cluster y Kubernetes con HPA.
Arquitectura de Laravel Queues + Redis en 2026
Colas con nombre y prioridades
Laravel permite organizar múltiples colas con nombre y diferentes prioridades. El worker procesa las colas en el orden indicado al iniciarse:
php artisan queue:work redis --queue=critical,high,default,low --sleep=3 --tries=3 --max-time=3600
En el código del job, la prioridad se define mediante la propiedad $queue o al despachar:
<?php
// Despachar a una cola específica
SendInvoice::dispatch($order)->onQueue('critical');
// O mediante la propiedad de la clase
class ProcessPayment implements ShouldQueue
{
public string $queue = 'critical';
public int $tries = 5;
public int $timeout = 120;
public int $backoff = 30;
}
Lotes de tareas (Job Batching)
El Job Batching, introducido en Laravel 8, se ha convertido en 2026 en el estándar para el procesamiento paralelo de grandes volúmenes de datos. Un batch permite lanzar miles de tareas y recibir un callback al finalizar:
<?php
use Illuminate\\Bus\\Batch;
use Illuminate\\Support\\Facades\\Bus;
$batch = Bus::batch([
new ImportChunk($chunk1),
new ImportChunk($chunk2),
new ImportChunk($chunk3),
])->then(function (Batch $batch) {
// Todas las tareas completadas con éxito
ImportCompleted::dispatch($batch->id);
})->catch(function (Batch $batch, Throwable $e) {
// Primer fallo en el batch
Log::error('Batch failed', ['batch_id' => $batch->id, 'error' => $e->getMessage()]);
})->finally(function (Batch $batch) {
// Siempre se ejecuta
Cache::forget('import_lock');
})->onQueue('high')
->dispatch();
Configuración de queue.php para Redis
<?php
// config/queue.php
return [
'default' => env('QUEUE_CONNECTION', 'redis'),
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'queue', // conexión Redis separada
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => 5, // BLPOP bloqueante en lugar de polling
'after_commit' => true, // despachar solo tras confirmar la transacción
],
],
];
El parámetro after_commit es crítico en producción: garantiza que la tarea llegue a la cola solo después de que la transacción se haya confirmado correctamente en la base de datos.
Configuración de Redis Cluster como backend para las colas
En sistemas de alta carga, una instancia Redis única se convierte en un cuello de botella. Redis Cluster distribuye los datos entre varios nodos y proporciona tolerancia a fallos.
Configura una conexión Redis separada para las colas en config/database.php:
<?php
// config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],
'queue' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_QUEUE_DB', '1'),
// Para Redis Cluster
'cluster' => [
['host' => 'redis-node-1', 'port' => 6379, 'password' => env('REDIS_PASSWORD')],
['host' => 'redis-node-2', 'port' => 6379, 'password' => env('REDIS_PASSWORD')],
['host' => 'redis-node-3', 'port' => 6379, 'password' => env('REDIS_PASSWORD')],
],
'options' => [
'cluster' => 'redis',
],
],
],
Punto importante: al usar Redis Cluster, todas las claves de una misma cola deben caer en el mismo slot. Laravel usa automáticamente hash tags ({queue_name}) para esto, pero asegúrate de que la versión de predis o la extensión phpredis soporta el modo cluster.
Contenerización de workers: Dockerfile y Supervisor
Dockerfile para el worker
FROM php:8.3-cli-alpine
# Dependencias del sistema
RUN apk add --no-cache \
supervisor \
git \
curl \
libpng-dev \
oniguruma-dev \
libxml2-dev
# Extensiones PHP
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath
# phpredis para mayor rendimiento
RUN pecl install redis && docker-php-ext-enable redis
# Opcache para CLI (acelera el arranque del worker)
RUN docker-php-ext-install opcache
# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
COPY . .
# Configuración de Supervisor
COPY docker/supervisor/worker.conf /etc/supervisor/conf.d/worker.conf
# Permisos
RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache
USER www-data
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
Configuración de Supervisor
; docker/supervisor/worker.conf
[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan queue:work redis \
--queue=critical,high,default,low \
--sleep=3 \
--tries=3 \
--max-time=3600 \
--memory=256 \
--timeout=90
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=%(ENV_WORKER_NUMPROCS)s
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stopwaitsecs=3600
La variable de entorno WORKER_NUMPROCS permite controlar el número de procesos dentro del contenedor. Combinado con Kubernetes, esto ofrece un escalado en dos niveles: número de pods × número de procesos dentro del pod.
Despliegue de workers como Kubernetes Deployment con HPA
Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-queue-worker
namespace: production
labels:
app: laravel-queue-worker
spec:
replicas: 3
selector:
matchLabels:
app: laravel-queue-worker
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2
maxUnavailable: 0 # zero-downtime durante actualizaciones
template:
metadata:
labels:
app: laravel-queue-worker
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9253"
spec:
terminationGracePeriodSeconds: 3600 # esperar a que finalicen tareas largas
containers:
- name: worker
image: your-registry/laravel-app:latest
imagePullPolicy: Always
env:
- name: APP_ENV
value: production
- name: QUEUE_CONNECTION
value: redis
- name: WORKER_NUMPROCS
value: "2"
envFrom:
- secretRef:
name: laravel-secrets
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
lifecycle:
preStop:
exec:
# Graceful shutdown: esperar a que la tarea actual finalice
command: ["/bin/sh", "-c", "php artisan queue:restart && sleep 30"]
restartPolicy: Always
Horizontal Pod Autoscaler (HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: laravel-queue-worker-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: laravel-queue-worker
minReplicas: 2
maxReplicas: 20
metrics:
# Escalado por CPU
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# Escalado por métrica personalizada (tamaño de cola desde Prometheus)
- type: External
external:
metric:
name: laravel_queue_size
selector:
matchLabels:
queue: default
target:
type: AverageValue
averageValue: "100" # 1 pod por cada 100 tareas en cola
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 4
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300 # reducimos despacio para evitar flapping
policies:
- type: Pods
value: 1
periodSeconds: 120
El escalado por métrica personalizada (tamaño de la cola Redis) es la ventaja clave de este enfoque. Cuando la cola crece, Kubernetes añade workers automáticamente; cuando se vacía, los reduce, ahorrando recursos.
Monitoreo de colas: Laravel Horizon y Prometheus
Configuración de Laravel Horizon
Laravel Horizon proporciona un dashboard elegante y estadísticas detalladas sobre las colas. Instalación:
composer require laravel/horizon
php artisan horizon:install
php artisan migrate
Configuración de config/horizon.php para entorno Kubernetes:
<?php
// config/horizon.php
return [
'use' => 'default',
'prefix' => env('HORIZON_PREFIX', 'horizon:'),
'middleware' => ['web', 'auth'],
'waits' => [
'redis:critical' => 3, // alerta si la tarea espera más de 3 seg
'redis:default' => 60,
'redis:low' => 300,
],
'trim' => [
'recent' => 60,
'pending' => 60,
'completed' => 60,
'recent_failed' => 10080, // 7 días
'failed' => 10080,
'monitored' => 10080,
],
'silenced' => [],
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
],
'fast_termination' => false,
'memory_limit' => 256,
'environments' => [
'production' => [
'supervisor-critical' => [
'connection' => 'redis',
'queue' => ['critical'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'minProcesses' => 2,
'maxProcesses' => 10,
'tries' => 3,
'timeout' => 90,
],
'supervisor-default' => [
'connection' => 'redis',
'queue' => ['high', 'default', 'low'],
'balance' => 'auto',
'autoScalingStrategy' => 'size',
'minProcesses' => 1,
'maxProcesses' => 20,
'balanceMaxShift' => 3,
'balanceCooldown' => 3,
'tries' => 3,
'timeout' => 120,
],
],
],
];
Métricas de Prometheus para Kubernetes HPA
Para exportar métricas de las colas de Laravel a Prometheus, usa el paquete spatie/laravel-prometheus o crea tu propio endpoint:
<?php
// app/Http/Controllers/MetricsController.php
namespace App\\Http\\Controllers;
use Illuminate\\Support\\Facades\\Redis;
class MetricsController extends Controller
{
public function index(): string
{
$queues = ['critical', 'high', 'default', 'low'];
$output = '';
foreach ($queues as $queue) {
$size = Redis::connection('queue')->llen('queues:' . $queue);
$output .= "laravel_queue_size{queue=\"$queue\"} $size\n";
}
// Conteo de failed jobs
$failedCount = Redis::connection('queue')->zcard('failed_jobs');
$output .= "laravel_queue_failed_jobs_total $failedCount\n";
return response($output, 200, ['Content-Type' => 'text/plain; version=0.0.4']);
}
}
Registra la ruta sin middleware en routes/api.php:
Route::get('/metrics', [MetricsController::class, 'index'])
->middleware('throttle:60,1');
Gestión de failed jobs y dead letter queue
Configuración de reintentos
En la clase del job, configura la estrategia de reintentos con backoff exponencial:
<?php
class ProcessWebhook implements ShouldQueue
{
public int $tries = 5;
public int $timeout = 60;
public int $maxExceptions = 3; // falla tras 3 excepciones, aunque no se agoten los tries
// Backoff exponencial: 10, 60, 180, 420 segundos
public function backoff(): array
{
return [10, 60, 180, 420];
}
public function handle(): void
{
// procesamiento del webhook
}
public function failed(Throwable $exception): void
{
// Notificación a Slack/PagerDuty en caso de fallo definitivo
Log::error('Webhook processing failed', [
'job_id' => $this->job->getJobId(),
'error' => $exception->getMessage(),
'payload' => $this->webhook->toArray(),
]);
// Enviamos a dead letter queue para análisis manual
DeadLetterQueue::push($this->webhook, $exception);
}
}
Dead Letter Queue mediante una cola separada
Implementa el patrón Dead Letter Queue mediante una cola con nombre dead-letter y un worker separado para monitoreo:
<?php
class DeadLetterQueue
{
public static function push(mixed $payload, Throwable $exception): void
{
Redis::connection('queue')->rpush('queues:dead-letter', json_encode([
'payload' => serialize($payload),
'exception' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
'failed_at' => now()->toISOString(),
]));
}
public static function requeue(string $jobId): void
{
// Lógica para redespachar desde la DLQ
Artisan::call('queue:retry', ['id' => [$jobId]]);
}
}
Comandos Artisan para gestionar failed jobs
# Listar tareas fallidas
php artisan queue:failed
# Reintentar una tarea específica
php artisan queue:retry 5d5a4fb3-5e82-4af3-bedf-6df64b4ee59b
# Reintentar todas las tareas fallidas
php artisan queue:retry all
# Limpiar tareas fallidas con más de 7 días
php artisan queue:prune-failed --hours=168
# Ver el estado de las colas con Horizon
php artisan horizon:status
Consejos de configuración en producción y errores comunes
Graceful shutdown en Kubernetes
Uno de los principales problemas es la terminación forzada del pod durante el escalado hacia abajo. Configura terminationGracePeriodSeconds igual al timeout máximo de la tarea y usa el hook preStop para enviar la señal queue:restart. Los workers terminarán las tareas actuales y se detendrán correctamente.
Fugas de memoria en workers de larga duración
PHP no libera memoria tan eficientemente como Go o Java. Usa los parámetros --max-time y --memory para que el worker se reinicie antes de alcanzar valores críticos. Supervisor lanzará automáticamente un nuevo proceso.
Idempotencia de las tareas
Diseña siempre las tareas de forma idempotente: ejecutar una tarea varias veces no debe producir efectos duplicados. Usa bloqueos de Redis para las secciones críticas:
<?php
public function handle(): void
{
$lock = Cache::lock('process-order-' . $this->orderId, 120);
if (! $lock->get()) {
Log::warning('Job already running, skipping', ['order_id' => $this->orderId]);
return;
}
try {
$this->processOrder();
} finally {
$lock->release();
}
}
Separación de colas por criticidad
No mezcles tareas de pago y envíos de newsletters en la misma cola. Destina Deployments separados para colas críticas con recursos garantizados y otros distintos para tareas en segundo plano con límites definidos.
Monitoreo del lag de cola
Configura alertas sobre el tiempo de espera de una tarea en cola (queue lag). Si una tarea en la cola critical espera más de 5 segundos, ya es un incidente. Laravel Horizon muestra este parámetro directamente en el dashboard.
Persistencia de Redis
Asegúrate de que Redis esté configurado con persistencia AOF (appendonly yes) o usa Redis Sentinel/Cluster. La pérdida de datos en la cola si Redis cae sin persistencia es un riesgo real en producción.
Conclusión
La combinación Laravel Queues + Redis Cluster + Kubernetes HPA en 2026 es un enfoque maduro y listo para producción para el escalado horizontal de tareas en segundo plano. Obtienes escalado automático de workers basado en el tamaño real de la cola, graceful shutdown sin pérdida de tareas, monitoreo detallado con Laravel Horizon y Prometheus, y gestión fiable de fallos mediante dead letter queue.
Los principios clave a recordar: las tareas deben ser idempotentes, los workers deben ser stateless y fáciles de reiniciar, y el monitoreo de las métricas de cola debe ser la base para las decisiones de autoescalado. Comienza con la configuración básica de este artículo y adáptala a las necesidades específicas de tu carga de trabajo.
Tecnologías
Etiquetas
Ruslan Ismailov
Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →