# Laravel Horizon Laravel Horizon is a dashboard and queue monitoring package for Laravel applications using Redis queues. It provides a beautiful web-based interface for monitoring key metrics such as job throughput, runtime, and failures, while offering code-driven configuration for worker management. Horizon stores all worker configuration in a single file, enabling teams to collaborate on queue infrastructure through version control. The package includes a comprehensive REST API for accessing queue statistics, job details, and worker information. It features automatic scaling of workers based on queue workload, configurable trimming of old job records, job tagging for monitoring specific workflows, and real-time notifications for long wait times. Horizon uses Redis as its data store and communicates with workers through Unix signals for process control. ## Installation and Setup Install Horizon via Composer and publish configuration ```bash composer require laravel/horizon # Publish assets and configuration php artisan horizon:install # Migrate Horizon's required database tables (if using database for failed jobs) php artisan migrate ``` ## Configuration Configure queue workers and supervisors ```php // config/horizon.php return [ 'domain' => env('HORIZON_DOMAIN'), 'path' => env('HORIZON_PATH', 'horizon'), 'use' => 'default', 'prefix' => env('HORIZON_PREFIX', 'laravel_horizon:'), 'middleware' => ['web'], // Queue wait time thresholds (seconds) 'waits' => [ 'redis:default' => 60, ], // Job retention times (minutes) 'trim' => [ 'recent' => 60, 'pending' => 60, 'completed' => 60, 'recent_failed' => 10080, 'failed' => 10080, 'monitored' => 10080, ], // Silence noisy jobs 'silenced' => [ // App\Jobs\ExampleJob::class, ], 'silenced_tags' => [ // 'notifications', ], // Metrics retention 'metrics' => [ 'trim_snapshots' => [ 'job' => 24, 'queue' => 24, ], ], 'fast_termination' => false, 'memory_limit' => 64, // Worker configuration 'defaults' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default'], 'balance' => 'auto', 'autoScalingStrategy' => 'time', 'maxProcesses' => 1, 'maxTime' => 0, 'maxJobs' => 0, 'memory' => 128, 'tries' => 1, 'timeout' => 60, 'nice' => 0, ], ], 'environments' => [ 'production' => [ 'supervisor-1' => [ 'maxProcesses' => 10, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], ], 'local' => [ 'supervisor-1' => [ 'maxProcesses' => 3, ], ], ], ]; ``` ## Authentication Customize dashboard access control ```php // app/Providers/HorizonServiceProvider.php use Laravel\Horizon\Horizon; class HorizonServiceProvider extends HorizonApplicationServiceProvider { public function boot() { parent::boot(); // Restrict dashboard access to authorized users Horizon::auth(function ($request) { return auth()->check() && auth()->user()->isAdmin(); }); } } ``` ## Notifications Configure Slack notifications for long wait times ```php // app/Providers/HorizonServiceProvider.php use Laravel\Horizon\Horizon; class HorizonServiceProvider extends HorizonApplicationServiceProvider { public function boot() { parent::boot(); // Route Slack notifications Horizon::routeSlackNotificationsTo( 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL', '#operations' ); // Route email notifications Horizon::routeMailNotificationsTo('ops@example.com'); // Route SMS notifications Horizon::routeSmsNotificationsTo('+15556667777'); } } ``` ## Start Horizon Run the master supervisor process ```bash # Start Horizon in the foreground php artisan horizon # Specify environment php artisan horizon --environment=production # Run as daemon using supervisor or systemd # See Laravel documentation for production deployment ``` ## Pause and Continue Pause job processing temporarily ```bash # Pause all workers (sends USR2 signal) php artisan horizon:pause # Resume processing (sends CONT signal) php artisan horizon:continue ``` ## Terminate and Restart Gracefully terminate Horizon ```bash # Terminate master supervisor (sends TERM signal) php artisan horizon:terminate # Fast termination without waiting for workers php artisan horizon:terminate --wait # After termination, restart Horizon php artisan horizon ``` ## Check Status View supervisor and worker status ```bash # Display current status php artisan horizon:status # List all supervisors php artisan horizon:supervisors # View supervisor details php artisan horizon:supervisor supervisor-1 ``` ## Purge Rogue Processes Clean up orphaned worker processes ```bash # Terminate rogue processes php artisan horizon:purge # Use custom signal php artisan horizon:purge --signal=SIGKILL ``` ## Clear Metrics and Jobs Remove old data from Redis ```bash # Clear all metrics php artisan horizon:clear-metrics # Clear completed and failed jobs php artisan horizon:clear # Forget specific failed job php artisan horizon:forget {id} ``` ## Dashboard API - Get Statistics Retrieve dashboard statistics ```bash curl http://your-app.com/horizon/api/stats ``` ```json { "failedJobs": 3, "jobsPerMinute": 125, "pausedMasters": 0, "periods": { "failedJobs": 10080, "recentJobs": 60 }, "processes": 10, "queueWithMaxRuntime": { "connection": "redis", "queue": "default", "runtime": 4523 }, "queueWithMaxThroughput": { "connection": "redis", "queue": "default", "throughput": 1250 }, "recentJobs": 7890, "status": "running", "wait": { "redis:default": 12 } } ``` ## Dashboard API - Get Workload Retrieve current queue workload ```bash curl http://your-app.com/horizon/api/workload ``` ```json [ { "name": "default", "length": 156, "wait": 12 }, { "name": "notifications", "length": 45, "wait": 3 } ] ``` ## Dashboard API - Get Master Supervisors List all master supervisors ```bash curl http://your-app.com/horizon/api/masters ``` ```json [ { "name": "master-abc123", "status": "running", "pid": 12345, "supervisors": [ { "name": "supervisor-1", "processes": 5, "options": { "connection": "redis", "queue": "default", "balance": "auto", "maxProcesses": 10 } } ] } ] ``` ## Dashboard API - Get Pending Jobs Retrieve pending jobs ```bash curl "http://your-app.com/horizon/api/jobs/pending?starting_at=0" ``` ```json { "jobs": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "connection": "redis", "queue": "default", "name": "App\\Jobs\\ProcessPodcast", "status": "pending", "payload": { "displayName": "App\\Jobs\\ProcessPodcast", "data": { "commandName": "App\\Jobs\\ProcessPodcast", "command": "..." } }, "index": 0 } ], "total": 156 } ``` ## Dashboard API - Get Completed Jobs Retrieve completed jobs ```bash curl "http://your-app.com/horizon/api/jobs/completed?starting_at=0" ``` ```json { "jobs": [ { "id": "650e8400-e29b-41d4-a716-446655440001", "connection": "redis", "queue": "default", "name": "App\\Jobs\\ProcessPodcast", "status": "completed", "payload": { "displayName": "App\\Jobs\\ProcessPodcast" }, "completed_at": "1704672000.123456", "index": 0 } ], "total": 7890 } ``` ## Dashboard API - Get Failed Jobs Retrieve failed jobs with filtering ```bash # Get all failed jobs curl "http://your-app.com/horizon/api/jobs/failed?starting_at=0" # Filter by tag curl "http://your-app.com/horizon/api/jobs/failed?tag=podcast&starting_at=0" ``` ```json { "jobs": [ { "id": "750e8400-e29b-41d4-a716-446655440002", "connection": "redis", "queue": "default", "name": "App\\Jobs\\ProcessPodcast", "status": "failed", "payload": { "displayName": "App\\Jobs\\ProcessPodcast", "data": { "commandName": "App\\Jobs\\ProcessPodcast" } }, "exception": "ErrorException: Undefined array key \"podcast_id\" in /app/Jobs/ProcessPodcast.php:42\nStack trace:\n...", "context": { "userId": 123, "podcastId": null }, "failed_at": "1704671000.654321", "retried_by": [ { "id": "850e8400-e29b-41d4-a716-446655440003", "status": "completed", "retried_at": 1704672000 } ], "index": 0 } ], "total": 3 } ``` ## Dashboard API - Get Single Job Retrieve job details by ID ```bash curl http://your-app.com/horizon/api/jobs/550e8400-e29b-41d4-a716-446655440000 ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "connection": "redis", "queue": "default", "name": "App\\Jobs\\ProcessPodcast", "status": "completed", "payload": { "displayName": "App\\Jobs\\ProcessPodcast", "data": { "commandName": "App\\Jobs\\ProcessPodcast", "command": "O:24:\"App\\Jobs\\ProcessPodcast\":..." } }, "completed_at": "1704672000.123456", "reserved_at": "1704671950.789012" } ``` ## Dashboard API - Retry Failed Job Retry a specific failed job ```bash curl -X POST http://your-app.com/horizon/api/jobs/retry/750e8400-e29b-41d4-a716-446655440002 ``` ## Dashboard API - Get Job Metrics Retrieve job performance metrics ```bash # List all job metrics curl http://your-app.com/horizon/api/metrics/jobs # Get specific job metrics curl http://your-app.com/horizon/api/metrics/jobs/App%5CJobs%5CProcessPodcast ``` ```json { "throughput": [ {"time": 1704672000, "value": 125}, {"time": 1704671940, "value": 130}, {"time": 1704671880, "value": 118} ], "runtime": [ {"time": 1704672000, "value": 2.5}, {"time": 1704671940, "value": 2.3}, {"time": 1704671880, "value": 2.8} ] } ``` ## Dashboard API - Get Queue Metrics Retrieve queue performance metrics ```bash # List all queue metrics curl http://your-app.com/horizon/api/metrics/queues # Get specific queue metrics curl http://your-app.com/horizon/api/metrics/queues/redis%3Adefault ``` ```json { "throughput": [ {"time": 1704672000, "value": 200}, {"time": 1704671940, "value": 195}, {"time": 1704671880, "value": 205} ], "runtime": [ {"time": 1704672000, "value": 1.8}, {"time": 1704671940, "value": 1.9}, {"time": 1704671880, "value": 1.7} ] } ``` ## Dashboard API - Monitor Tags Start and stop monitoring specific job tags ```bash # Start monitoring a tag curl -X POST http://your-app.com/horizon/api/monitoring \ -H "Content-Type: application/json" \ -d '{"tag": "podcast"}' # Get monitored tags curl http://your-app.com/horizon/api/monitoring ``` ```json [ { "tag": "podcast", "count": 45 }, { "tag": "email", "count": 123 } ] ``` ```bash # Get jobs for monitored tag curl "http://your-app.com/horizon/api/monitoring/podcast?tag=podcast&starting_at=0&limit=25" ``` ```json { "jobs": [ { "id": "950e8400-e29b-41d4-a716-446655440004", "name": "App\\Jobs\\ProcessPodcast", "status": "completed", "payload": { "displayName": "App\\Jobs\\ProcessPodcast" } } ], "total": 45 } ``` ```bash # Stop monitoring a tag curl -X DELETE http://your-app.com/horizon/api/monitoring/podcast ``` ## Dashboard API - Get Batches Retrieve job batches ```bash # List all batches curl http://your-app.com/horizon/api/batches # Get specific batch curl http://your-app.com/horizon/api/batches/9a5e8400-e29b-41d4-a716-446655440005 ``` ```json { "id": "9a5e8400-e29b-41d4-a716-446655440005", "name": "Import Podcasts", "totalJobs": 100, "pendingJobs": 15, "failedJobs": 2, "processedJobs": 83, "progress": 83, "createdAt": 1704670000, "finishedAt": null, "cancelledAt": null } ``` ```bash # Retry failed batch jobs curl -X POST http://your-app.com/horizon/api/batches/retry/9a5e8400-e29b-41d4-a716-446655440005 ``` ## Job Repository Programmatically interact with job records ```php use Laravel\Horizon\Contracts\JobRepository; class DashboardController { public function __construct( private JobRepository $jobs ) {} public function recentStats() { // Count jobs by status $recentCount = $this->jobs->countRecent(); $pendingCount = $this->jobs->countPending(); $completedCount = $this->jobs->countCompleted(); $failedCount = $this->jobs->countFailed(); $silencedCount = $this->jobs->countSilenced(); // Get paginated job lists $recentJobs = $this->jobs->getRecent($afterIndex = 0); $pendingJobs = $this->jobs->getPending($afterIndex = 0); $completedJobs = $this->jobs->getCompleted($afterIndex = 0); $failedJobs = $this->jobs->getFailed($afterIndex = 0); // Get specific jobs by IDs $jobs = $this->jobs->getJobs(['job-id-1', 'job-id-2']); // Find failed job $failedJob = $this->jobs->findFailed('job-id-123'); // Delete failed job $this->jobs->deleteFailed('job-id-123'); return [ 'counts' => compact( 'recentCount', 'pendingCount', 'completedCount', 'failedCount' ), 'recent' => $recentJobs, ]; } } ``` ## Auto Scaling Horizon automatically scales workers based on queue workload ```php // config/horizon.php - Auto scaling configuration 'environments' => [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default', 'notifications'], 'balance' => 'auto', // Enable auto-scaling 'autoScalingStrategy' => 'time', // Scale by time or 'size' by jobs 'minProcesses' => 1, 'maxProcesses' => 10, 'balanceMaxShift' => 1, // Max workers to add/remove per cycle 'balanceCooldown' => 3, // Seconds between scaling operations 'tries' => 3, 'timeout' => 300, 'memory' => 128, ], ], ], ``` ## Long Wait Detection Event Handle long queue wait times ```php use Laravel\Horizon\Events\LongWaitDetected; use Illuminate\Support\Facades\Log; class EventServiceProvider extends ServiceProvider { protected $listen = [ LongWaitDetected::class => [ LogLongWait::class, ScaleUpWorkers::class, ], ]; } class LogLongWait { public function handle(LongWaitDetected $event) { Log::warning('Queue wait time exceeded', [ 'connection' => $event->connection, 'queue' => $event->queue, 'seconds' => $event->seconds, ]); // Send notification $notification = $event->toNotification(); // Send via configured channels (Slack, Email, SMS) } } ``` ## Programmatic Control Control Horizon programmatically ```php use Laravel\Horizon\Contracts\MasterSupervisorRepository; use Laravel\Horizon\Contracts\SupervisorRepository; use Laravel\Horizon\MasterSupervisor; class HorizonManager { public function __construct( private MasterSupervisorRepository $masters, private SupervisorRepository $supervisors ) {} public function status(): array { // Check if Horizon is running $allMasters = $this->masters->all(); if (empty($allMasters)) { return ['status' => 'inactive']; } // Get master supervisor names $masterNames = $this->masters->names(); // Check if all are paused $allPaused = collect($allMasters)->every( fn($master) => $master->status === 'paused' ); // Get all supervisors $allSupervisors = $this->supervisors->all(); return [ 'status' => $allPaused ? 'paused' : 'running', 'masters' => count($allMasters), 'supervisors' => count($allSupervisors), ]; } public function pause(): bool { $masters = collect($this->masters->all()) ->filter(fn($master) => str_starts_with($master->name, MasterSupervisor::basename()) ); foreach ($masters as $master) { posix_kill($master->pid, SIGUSR2); } return true; } public function continue(): bool { $masters = collect($this->masters->all()) ->filter(fn($master) => str_starts_with($master->name, MasterSupervisor::basename()) ); foreach ($masters as $master) { posix_kill($master->pid, SIGCONT); } return true; } public function terminate(): bool { $masters = collect($this->masters->all()) ->filter(fn($master) => str_starts_with($master->name, MasterSupervisor::basename()) ); foreach ($masters as $master) { posix_kill($master->pid, SIGTERM); } // Trigger queue restart cache()->forever('illuminate:queue:restart', now()->timestamp); return true; } } ``` ## Scheduled Tasks Add Horizon maintenance tasks to scheduler ```php // app/Console/Kernel.php use Illuminate\Console\Scheduling\Schedule; class Kernel extends ConsoleKernel { protected function schedule(Schedule $schedule) { // Take snapshots for metrics every 5 minutes $schedule->command('horizon:snapshot')->everyFiveMinutes(); // Clear old metrics hourly $schedule->command('horizon:clear-metrics')->hourly(); // Purge old job records daily $schedule->command('horizon:clear')->daily(); // Clean up orphaned processes every 15 minutes $schedule->command('horizon:purge')->everyFifteenMinutes(); } } ``` ## Summary Laravel Horizon transforms queue management for Laravel applications by providing comprehensive monitoring, automatic scaling, and operational control through both a web dashboard and programmatic API. Its core use cases include monitoring production queue health, debugging failed jobs with detailed stack traces and context, scaling workers automatically based on workload, and receiving alerts when queues experience delays. The REST API enables integration with external monitoring systems and custom dashboards. Teams typically deploy Horizon alongside a process supervisor like systemd or Supervisor to ensure the master process restarts after crashes or deployments. The code-driven configuration approach means infrastructure changes are reviewed through pull requests, while the automatic scaling reduces manual intervention for traffic spikes. Job tagging and monitoring features help track specific workflows through the system, and the built-in retention policies keep Redis memory usage under control by automatically trimming old job records.