Laravel Horizon: Monitor and Scale Your Queues

Khimananda Oli 6 min read DevOps
Laravel Horizon: Monitor and Scale Your Queues

By Khimananda Oli | Last reviewed: August 2026

Running background jobs without visibility is a liability in any production environment. Laravel Horizon: Monitor and Scale Your Queues effectively by combining real-time Redis metrics with intelligent supervisor configurations that adapt to actual workload demand. This guide covers the exact configuration patterns I use to keep job throughput stable and costs predictable, whether you are deploying to a single VPS or orchestrating containers via Docker for Laravel applications.

How does Laravel Horizon monitor and scale your queues automatically?

Horizon differs from standard Laravel queue workers because it acts as a process manager on top of Redis. Instead of running static php artisan queue:work commands, Horizon spawns and kills worker processes dynamically based on queue wait times. When you configure Laravel Horizon to monitor and scale your queues, you are essentially defining a feedback loop where the system measures backlog depth and adjusts compute resources within boundaries you set.

Redis QueueHorizon Master(Metrics & Logic)Worker Pool(Auto-Scaled)Job Completion Feedback Loop
Horizon monitors Redis wait times and dynamically adjusts worker processes within defined min/max boundaries.

The core mechanism relies on three configuration keys in your supervisor definition:

  • balance: Set to auto for production. This tells Horizon to shift workers between queues based on current wait time ratios rather than fixed counts.
  • minProcesses / maxProcesses: Hard floors and ceilings. Never leave these unbounded in production; a sudden spike could exhaust server memory.
  • balanceCooldown: The number of seconds Horizon waits before re-evaluating. A value of 3–10 seconds prevents process thrashing during bursty traffic.

What is the optimal Horizon configuration for production workloads?

A common mistake is copying development configs directly to production. In practice, you need environment-specific supervisor arrays. Below is a battle-tested configuration structure for a typical SaaS application handling emails, data imports, and report generation.

// config/horizon.php
'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['high', 'default', 'low'],
            'balance' => 'auto',
            'minProcesses' => 4,
            'maxProcesses' => 20,
            'maxTime' => 3600,
            'maxJobs' => 1000,
            'memory' => 256,
            'tries' => 3,
            'timeout' => 300,
            'nice' => 0,
        ],
        'supervisor-imports' => [
            'connection' => 'redis-long',
            'queue' => ['imports'],
            'balance' => 'simple',
            'processes' => 3,
            'memory' => 512,
            'timeout' => 1800,
        ],
    ],
],

Why separate long-running jobs?

Import jobs often take minutes and consume significant memory. If they share a supervisor with fast email jobs, they can block the pool or trigger OOM kills that disrupt high-priority traffic. Dedicated supervisors with simple balancing and higher memory limits isolate this risk. For teams managing infrastructure via code, defining these parameters in Terraform or Ansible ensures consistency across staging and production.

How do you tune Redis for reliable Laravel queue performance?

Horizon is only as fast as its Redis backend. When configuring Laravel Horizon to monitor and scale your queues, Redis tuning is non-negotiable. Default Redis settings often bottleneck at 5,000+ jobs per minute.

ParameterDefaultProduction RecommendationRationale
maxmemory-policynoevictionvolatile-lruPrevents OOM crashes; evicts expiring keys first while preserving queue data
tcp-backlog5112048+Handles connection bursts during deployments or traffic spikes
saveMultiple RDB snapshots"" (disable) if using AOFRDB forks cause latency spikes; rely on AOF fsync=everysec for durability
hz10100Faster expiration checks and timeout handling for queue workloads

In Nepal’s growing tech ecosystem, many teams still run Redis on the same instance as their web server. This works for low traffic but fails under load. Dedicate at least a separate container or VM for Redis once you exceed 50 concurrent workers. If you are hosting on AWS, consider ElastiCache with cluster mode disabled for simpler Horizon compatibility, as documented in my guide on hosting Laravel on AWS EC2 and RDS.

How should you deploy and supervise Horizon in Docker or Kubernetes?

Horizon itself manages PHP worker processes, but something must manage the Horizon master process. In containerized environments, this distinction causes frequent failures when misconfigured.

Container Runtime (Docker / K8s Pod)Entrypoint ScriptHorizon MasterWorker Process 1..NWorker Process N+1..M
Proper container entrypoints must exec into Horizon so signals propagate correctly to worker processes.
  1. Use exec in your entrypoint: Your Dockerfile CMD should be ["php", "artisan", "horizon"] or an entrypoint script that uses exec php artisan horizon. Without exec, SIGTERM signals from Kubernetes or Docker stop won’t reach Horizon, causing graceful shutdown failures and duplicate job processing.
  2. Set termination grace period: Match your Kubernetes terminationGracePeriodSeconds or Docker stop timeout to Horizon’s --timeout plus buffer. If your longest job takes 300s, set grace period to 360s minimum.
  3. Liveness probes: Use php artisan horizon:status or check the /horizon/api/stats endpoint. Do not probe individual worker PIDs; Horizon manages those internally.
  4. Resource requests vs limits: Set memory requests to maxProcesses × memory + 20% overhead. CPU requests should reflect average load, not peak, since Horizon scales workers up and down.

How do you debug stuck queues and scaling issues in Horizon?

Even with perfect configuration, queues stall. Here is the diagnostic sequence I follow when on-call:

  • Check horizon:status: Returns running, paused, or inactive. If paused, someone ran horizon:pause during deployment and forgot to resume.
  • Inspect recent failed jobs: php artisan horizon:failed shows failures with context. Correlate timestamps with deployment logs or infrastructure events.
  • Verify Redis connectivity: Run redis-cli -h <host> ping from the application container. Intermittent timeouts often indicate network saturation or Redis fork latency during BGSAVE.
  • Review supervisor balance logs: Horizon logs scaling decisions to storage/logs/horizon.log. Look for rapid scale-up/scale-down cycles indicating an overly aggressive balanceCooldown.

If jobs are processing but slower than expected, profile the job itself before blaming Horizon. Database locks, external API rate limits, and missing indexes are far more common culprits than queue misconfiguration. My article on Laravel performance optimization techniques covers profiling methods that apply directly to queued jobs.

Queue Stalled?horizon:status = running?NoResume / RestartYesCheck Failed JobsRedis Healthy?NoFix Redis / NetYesProfile Job Code
Systematic troubleshooting flowchart for Laravel Horizon queue diagnosis in production.

Implementing Laravel Horizon to Monitor and Scale Your Queues Reliably

Getting Laravel Horizon to monitor and scale your queues reliably requires treating it as infrastructure, not just a package. Define supervisor strategies per environment, dedicate Redis resources, enforce graceful shutdown semantics in containers, and establish a clear debugging playbook before incidents occur. These practices have kept queue systems stable for me across AWS, bare-metal, and hybrid deployments serving both global and Nepali markets.

If your team needs help auditing your current queue architecture or designing a compliant, observable job processing pipeline, reach out to discuss your infrastructure. Whether you’re preparing for SOC 2 evidence collection or simply tired of 3 AM pager alerts, we can build a system that scales predictably and sleeps soundly.

Frequently Asked Questions

Horizon provides a real-time dashboard and code-driven configuration for Redis queues. It replaces manual supervisor tuning with auto-scaling, job tagging, and failure metrics specifically designed for Laravel applications running high-throughput background workloads in production environments.

Require the package via Composer using version 6.x for Laravel 12 compatibility. Publish the configuration file and run migrations to create the monitoring tables. Finally, deploy the horizon worker process alongside your standard queue workers to enable dashboard functionality.

No. Horizon strictly requires Redis as the queue connection. It relies on Redis data structures for real-time metrics, tags, and job tracking. Use standard Laravel queue monitoring tools if you must stick with database, SQS, or Beanstalkd drivers.

Horizon adjusts worker counts based on configurable workload ratios rather than CPU usage. You define min and max processes per supervisor; Horizon spawns or kills workers dynamically as pending jobs accumulate or drain, ensuring optimal throughput without over-provisioning resources.

Yes. Define multiple supervisors in horizon.php with different queue assignments and scaling parameters. This isolates critical jobs like emails from resource-heavy tasks like video processing, preventing head-of-line blocking while maintaining independent auto-scaling policies for each workload type.

Simple maintains a fixed number of workers regardless of load. Auto dynamically scales workers between defined minimums and maximums based on current queue depth. Choose auto for variable traffic patterns and simple for predictable, consistent background processing workloads.

Restrict access using the gate callback in HorizonServiceProvider. Typically check Auth::check() and verify admin roles or specific permissions. Never expose the dashboard publicly without authentication, as it reveals sensitive job data, environment variables, and internal system architecture details.

Verify that php artisan horizon is running instead of standard queue:work commands. Standard workers bypass Horizon entirely. Also confirm your Redis connection matches the one configured in horizon.php and that no firewall rules block communication between PHP-FPM and the Redis instance.

Typical Laravel workers consume 30MB to 80MB depending on application bootstrapping and loaded services. Monitor actual usage with htop or Datadog. Set max_memory in supervisor config to automatically restart workers before they exceed available RAM and trigger OOM kills.

No. Horizon only tracks queued jobs dispatched through Laravel's queue system. Scheduled commands run via scheduler operate outside the queue pipeline. Use Laravel Pulse or external uptime monitors to track command execution times, failures, and scheduler health separately.

Failed jobs appear in the Failed Jobs tab with full exception traces and payload inspection. Configure retry limits and backoff strategies in job classes. Use the dashboard to manually retry specific failures after fixing underlying issues without redeploying or restarting workers.

Yes. Run horizon as a separate deployment or sidecar container. Configure horizontal pod autoscaling based on custom Redis metrics exported by Prometheus adapters. Ensure persistent Redis connectivity across pod restarts and use graceful shutdown hooks to prevent job interruption during scaling events.

Workers pause processing and log connection errors until Redis recovers. Jobs remain safe in client-side buffers or retry mechanisms. Configure Redis Sentinel or Cluster for high availability. Horizon automatically reconnects without manual intervention once the Redis service becomes reachable again.

Deploy new code first, then run php artisan horizon:terminate gracefully. Active workers finish current jobs before exiting. Supervisors respawn with updated configuration. Never kill workers forcefully during deployments to prevent duplicate processing or lost jobs in production environments.

Minimal. Horizon adds roughly five percent overhead for metrics collection and Redis writes. The trade-off for observability and auto-scaling justifies this cost. Disable telemetry in extremely high-throughput scenarios exceeding ten thousand jobs per minute where raw performance outweighs monitoring needs.