laravelqueuesoptimizacionroadmap

Laravel Horizon: gestión de colas Redis en producción

Las colas en Laravel son una herramienta fundamental para mover tareas lentas fuera del ciclo de la petición HTTP: enviar emails, procesar imágenes, generar reportes, llamar a APIs externas. Cuando usas Redis como driver de colas (la opción más potente), Laravel Horizon es el panel de control que necesitas para gestionar y monitorizar todo ese trabajo en background.

¿Qué es Horizon y por qué Redis?

Laravel tiene soporte para varios drivers de colas: database, Redis, Beanstalkd, Amazon SQS. Redis es el más usado en producción porque es extremadamente rápido (opera en memoria), soporta colas con prioridades, es fiable y tiene excelente soporte en cualquier servidor.

Laravel Horizon es el panel de administración oficial para colas Redis. Proporciona:

  • Dashboard en tiempo real con métricas de jobs
  • Visualización de jobs en cola, procesando y fallidos
  • Configuración declarativa de workers (supervisores)
  • Balanceo automático de procesos según la carga
  • Alertas cuando las colas superan un umbral
  • Reintentos de jobs fallidos

Sin Horizon, gestionar workers de colas en producción es tedioso. Con Horizon, tienes visibilidad total.

Requisitos previos

Antes de instalar Horizon necesitas:

  • Laravel 10 o superior
  • Redis instalado y funcionando
  • La extensión phpredis o el paquete predis/predis
  • Variables de entorno configuradas para Redis
# Verificar que Redis está corriendo
redis-cli ping
# PONG

# Instalar predis si no tienes la extensión phpredis
composer require predis/predis

En tu .env:

QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Instalar Horizon

composer require laravel/horizon
php artisan horizon:install

El comando horizon:install publica:

  • El archivo de configuración config/horizon.php
  • Los assets del dashboard en public/vendor/horizon
  • El HorizonServiceProvider en app/Providers

Después, ejecuta las migraciones (Horizon necesita una tabla para jobs fallidos):

php artisan migrate

Configuración: config/horizon.php

Este es el archivo más importante. Vamos a ver las partes clave:

// config/horizon.php
return [
    'domain'  => env('HORIZON_DOMAIN'),
    'path'    => env('HORIZON_PATH', 'horizon'),
    'use'     => 'default',
    'prefix'  => env('HORIZON_PREFIX', 'horizon:'),

    // Tiempo máximo de espera para que un job complete al hacer pausa
    'waits' => [
        'redis:default' => 60,
    ],

    // Número de segundos antes de que Horizon notifique sobre workers lentos
    'trim' => [
        'recent'        => 60,
        'pending'       => 60,
        'completed'     => 60,
        'recent_failed' => 10080,
        'failed'        => 10080,
        'monitored'     => 10080,
    ],

    'silenced' => [
        // Jobs que no quieres que aparezcan en el dashboard
    ],

    'metrics' => [
        'trim_snapshots' => [
            'job'   => 24,
            'queue' => 24,
        ],
    ],

    'fast_termination' => false,

    'memory_limit' => 64,

    'environments' => [
        'production' => [
            'supervisor-1' => [
                'connection'   => 'redis',
                'queue'        => ['default'],
                'balance'      => 'auto',
                'autoScalingStrategy' => 'time',
                'minProcesses' => 1,
                'maxProcesses' => 10,
                'balanceMaxShift'    => 1,
                'balanceCooldown'    => 3,
                'tries'        => 3,
                'timeout'      => 60,
                'nice'         => 0,
            ],
        ],

        'local' => [
            'supervisor-1' => [
                'connection'   => 'redis',
                'queue'        => ['default'],
                'balance'      => 'simple',
                'minProcesses' => 1,
                'maxProcesses' => 3,
                'tries'        => 1,
                'timeout'      => 60,
            ],
        ],
    ],
];

Supervisores: el corazón de Horizon

Los supervisores son grupos de workers que Horizon gestiona. Cada supervisor tiene su configuración:

  • connection: el driver de cola (redis)
  • queue: lista de colas que procesa este supervisor (en orden de prioridad)
  • balance: estrategia de balanceo (simple, auto, false)
  • minProcesses/maxProcesses: número mínimo y máximo de workers
  • tries: número de reintentos antes de marcar el job como fallido
  • timeout: segundos antes de matar un job que lleva demasiado tiempo

Múltiples supervisores para diferentes prioridades

'environments' => [
    'production' => [
        // Workers para emails urgentes (alta prioridad)
        'supervisor-emails' => [
            'connection'   => 'redis',
            'queue'        => ['emails-urgentes', 'emails'],
            'balance'      => 'auto',
            'minProcesses' => 2,
            'maxProcesses' => 8,
            'tries'        => 3,
            'timeout'      => 30,
        ],

        // Workers para procesamiento de imágenes (requiere más memoria)
        'supervisor-media' => [
            'connection'   => 'redis',
            'queue'        => ['media'],
            'balance'      => 'simple',
            'minProcesses' => 1,
            'maxProcesses' => 4,
            'tries'        => 2,
            'timeout'      => 300,
        ],

        // Workers para tareas de bajo peso general
        'supervisor-default' => [
            'connection'   => 'redis',
            'queue'        => ['default', 'low'],
            'balance'      => 'auto',
            'minProcesses' => 1,
            'maxProcesses' => 5,
            'tries'        => 3,
            'timeout'      => 60,
        ],
    ],
],

Para que un job use una cola específica:

// app/Jobs/ProcesarImagen.php
class ProcesarImagen implements ShouldQueue
{
    public $queue = 'media';
    public $timeout = 300;
    public $tries = 2;

    // ...
}

// O al despachar
ProcesarImagen::dispatch($imagen)->onQueue('media');

Ejecutar Horizon

php artisan horizon

Horizon muestra los logs en tiempo real en la terminal. Para detenerlo de forma ordenada (espera a que los jobs activos terminen):

php artisan horizon:terminate

El Dashboard

Accede al dashboard en http://tu-dominio.com/horizon. Por defecto solo es accesible en entorno local. Para producción, configura la autorización en el HorizonServiceProvider:

// app/Providers/HorizonServiceProvider.php
use Laravel\Horizon\Horizon;

protected function gate(): void
{
    Gate::define('viewHorizon', function (User $user) {
        return in_array($user->email, [
            'admin@tudominio.com',
            'devops@tudominio.com',
        ]);
    });
}

El dashboard muestra:

  • Jobs por segundo en tiempo real
  • Jobs completados en los últimos minutos/horas
  • Jobs fallidos con trazas de error completas
  • Tiempo promedio de ejecución por job
  • Estado de los supervisores y número de procesos activos

Monitorizar colas específicas

Puedes configurar Horizon para que envíe notificaciones cuando una cola supera cierto número de jobs pendientes:

// app/Providers/HorizonServiceProvider.php
use Laravel\Horizon\Horizon;

public function boot(): void
{
    parent::boot();

    // Notificar si cualquier cola supera 300 jobs pendientes
    Horizon::routeMailNotificationsTo('admin@tudominio.com');
    Horizon::routeSlackNotificationsTo(env('SLACK_WEBHOOK_URL'), '#horizon-alerts');

    // Umbrales por cola
    Horizon::night();
}

Etiquetas para monitorizar jobs

Horizon permite etiquetar jobs para filtrarlos y monitorizar objetos específicos:

// En tu job
class NotificarUsuario implements ShouldQueue
{
    public function __construct(protected User $user)
    {
    }

    public function tags(): array
    {
        return [
            'notificacion',
            'usuario:' . $this->user->id,
        ];
    }

    // ...
}

Después puedes buscar en el dashboard todos los jobs relacionados con usuario:42, por ejemplo.

Pausar y reanudar Horizon

En situaciones de emergencia (deploy, mantenimiento) puedes pausar Horizon sin perder jobs:

# Pausar (los jobs quedan en cola, los workers terminan el actual y esperan)
php artisan horizon:pause

# Pausar un supervisor específico
php artisan horizon:pause-supervisor supervisor-media

# Reanudar
php artisan horizon:continue

# Reanudar un supervisor específico
php artisan horizon:continue-supervisor supervisor-media

# Ver el estado actual
php artisan horizon:status

Jobs fallidos: reintento y limpieza

Cuando un job falla todas sus reintentos, se guarda en la tabla failed_jobs. Horizon proporciona una interfaz visual para gestionarlos.

Por línea de comandos:

# Ver todos los jobs fallidos
php artisan queue:failed

# Reintentar un job específico por UUID
php artisan queue:retry abc-123-def-456

# Reintentar todos los fallidos
php artisan queue:retry all

# Eliminar un job fallido
php artisan horizon:forget abc-123-def-456

# Limpiar todos los jobs fallidos
php artisan horizon:clear

También puedes manejar el fallo en el propio job:

class EnviarEmail implements ShouldQueue
{
    public $tries = 3;
    public $backoff = [10, 30, 60]; // Espera entre reintentos en segundos

    public function failed(\Throwable $exception): void
    {
        // Notificar al admin cuando el job falla definitivamente
        \Log::error('Job EnviarEmail falló definitivamente', [
            'exception' => $exception->getMessage(),
            'user_id'   => $this->user->id,
        ]);
    }
}

Desplegar Horizon en producción con Supervisor

En producción, Horizon debe ejecutarse como un servicio que se reinicie automáticamente si falla. La herramienta estándar para esto es Supervisor (el gestor de procesos de Linux, no confundir con los supervisores de Horizon).

Crea el archivo de configuración:

; /etc/supervisor/conf.d/horizon.conf
[program:horizon]
process_name=%(program_name)s
command=php /var/www/html/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/horizon.log
stopwaitsecs=3600

Activa y arranca el proceso:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start horizon

Reiniciar Horizon en el deploy

Cuando despliega código nuevo, debes reiniciar Horizon para que los workers carguen los nuevos cambios. Añade esto a tu script de deploy:

php artisan horizon:terminate

Supervisor detectará que Horizon se ha detenido y lo reiniciará automáticamente con el nuevo código.

Si usas Laravel Forge, el servidor ya incluye Supervisor configurado y puedes gestionar el daemon de Horizon directamente desde el panel.

Conclusión

Laravel Horizon transforma la gestión de colas de una tarea compleja en algo completamente manejable. El dashboard en tiempo real, la configuración declarativa de workers y las herramientas para gestionar jobs fallidos hacen que mantener colas en producción sea mucho menos estresante. Si tu aplicación usa colas Redis (y debería si procesa tareas pesadas), Horizon no es opcional: es imprescindible.