
August 12, 2026
9 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Choosing between Laravel Horizon vs Supervisor for queue workers determines whether your background jobs are observable, auto-scaling, and recoverable, or simply running as opaque processes. Many developers I work with on Laravel projects in Nepal start with plain Supervisor because it feels simpler, then migrate to Horizon once job volume grows or debugging becomes painful. This guide breaks down the architectural differences, real configuration patterns for Laravel 12 and PHP 8.4, and the specific trade-offs that matter in production environments.
What Is the Difference Between Laravel Horizon and Supervisor?
Supervisor is a Linux-level process control system. It knows nothing about Laravel, queues, or business logic. Its only job is to ensure that a specified number of processes (like php artisan queue:work) remain running, restarting them if they crash. It writes logs to files and has no web interface.
Laravel Horizon is an application-level package built specifically for Laravel’s Redis queue driver. It provides a real-time dashboard, configurable auto-scaling strategies, job tagging, failure tracking, and metrics. Crucially, Horizon itself runs under Supervisor. Horizon does not replace Supervisor; it replaces manual queue:work commands with intelligent worker pools managed through a configuration file.
In practice, the decision is rarely "one or the other." For any Laravel application using Redis queues in 2026, Supervisor is mandatory infrastructure. Horizon is the optional but highly recommended application layer that makes queue operations visible and manageable. If you are building high-traffic Laravel queue systems, skipping Horizon means accepting significant operational blind spots.
How Do You Configure Supervisor for Laravel Queue Workers?
Supervisor configuration lives in /etc/supervisor/conf.d/ on Ubuntu 22.04/24.04. Each Laravel application should have its own .conf file. Below is a production-tested configuration for Laravel 12 running on PHP 8.4:
[program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/myapp/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --memory=256 autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=4 redirect_stderr=true stdout_logfile=/var/www/myapp/shared/storage/logs/worker.log stopwaitsecs=3600Key parameters explained from real deployment experience:
- --max-time=3600: Forces each worker to restart after one hour. This prevents memory leaks from accumulating in long-running PHP processes. On legal-tech portals I maintain, this single flag eliminated weekly OOM kills.
- --memory=256: Sets a hard memory ceiling per worker. Jobs exceeding this cause a graceful worker restart rather than silent failures.
- --sleep=3: Seconds to wait before polling for new jobs when the queue is empty. Lower values increase Redis load; higher values add latency. Three seconds is a sensible default for most workloads.
- --tries=3: Maximum attempts before marking a job as failed. Always pair this with exponential backoff in your job class.
- stopwaitsecs=3600: Must be ≥ your longest expected job duration. If Supervisor kills a worker mid-job, you get duplicate processing or data corruption.
After creating the config file, always run:
sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start laravel-worker:*A common mistake on shared hosting or budget VPS setups in Nepal is setting numprocs too high relative to available RAM. Each worker consumes 50–150 MB depending on application bootstrapping. Four workers at 256 MB limit plus OS overhead requires at least 2 GB dedicated RAM. Monitor with htop before increasing process counts.
When Should You Use Laravel Horizon Instead of Plain Supervisor?
Horizon justifies its additional complexity when your application meets any of these conditions:
- Multiple queue priorities: You have separate queues like
emails,invoices,importsand need different worker counts or retry policies per queue. - Traffic spikes are unpredictable: E-commerce flash sales, document processing bursts during Nepali fiscal year-end, or webhook floods from payment gateways like eSewa or Khalti require dynamic scaling.
- Debugging failed jobs is frequent: Your team spends time SSH-ing into servers to grep log files instead of using a searchable failure UI.
- You need throughput metrics: Business stakeholders ask "how many orders processed per minute?" or "what's the average email delivery time?"
- Zero-downtime deployments matter: Horizon’s
horizon:terminatecommand gracefully finishes current jobs before deploying new code, preventing job loss during releases.
If your application processes fewer than 1,000 jobs per day with uniform priority and predictable load, plain Supervisor with static workers is perfectly adequate. Horizon adds value proportional to operational complexity, not merely job volume.
How Does Laravel Horizon Auto-Scaling Work in Production?
Horizon’s auto-scaling is configured in config/horizon.php under environment-specific arrays. Unlike Kubernetes HPA which scales pods, Horizon adjusts the number of worker processes within a single server based on queue backlog:
'environments' => [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default', 'emails', 'invoices'], 'balance' => 'auto', 'minProcesses' => 2, 'maxProcesses' => 12, 'maxTime' => 3600, 'maxJobs' => 1000, 'memory' => 256, 'tries' => 3, 'nice' => 0, ], ], ],The balance => 'auto' strategy uses a cooldown algorithm. When pending jobs exceed available workers, Horizon spawns new processes up to maxProcesses. When the queue drains, it waits through a cooldown period before terminating excess workers. This prevents rapid spawn/kill cycles during traffic plateaus.
On a legal document processing platform I built, we set minProcesses=2 for baseline email notifications and maxProcesses=8 for bulk attestation uploads during peak government filing seasons. The key insight: maxProcesses must respect server RAM. Eight workers at 256 MB each plus Redis, Nginx, PHP-FPM, and MySQL requires minimum 4 GB RAM on a single EC2 instance. Always calculate before deploying.
For multi-server deployments, each server runs its own Horizon instance with identical config. Horizon coordinates via Redis locks, so two servers won’t double-process the same job. This is where Redis as a coordination layer proves essential beyond simple queue storage.
Laravel Horizon vs Supervisor: Feature Comparison Table
| Feature | Supervisor Only | Horizon + Supervisor |
|---|---|---|
| Process Management | ✅ Core function | ✅ Via Supervisor underneath |
| Web Dashboard | ❌ None | ✅ Real-time Vue.js UI |
| Auto-Scaling Workers | ❌ Static numprocs only | ✅ Balance strategies (auto/simple/false) |
| Failed Job Inspection | ❌ Log files only | ✅ Searchable UI with payload/retry |
| Job Tagging & Filtering | ❌ Not possible | ✅ Model-based automatic tags |
| Throughput Metrics | ❌ Manual log parsing | ✅ Per-queue jobs/minute, runtime |
| Graceful Deploy Termination | ⚠️ Manual SIGTERM handling | ✅ horizon:terminate built-in |
| Queue Driver Support | ✅ Any (database, sqs, redis) | ❌ Redis only |
| Setup Complexity | Low (~15 min) | Medium (~45 min + Redis) |
| Server Resource Overhead | Negligible | +Redis memory, +PHP master process |
The critical constraint: Horizon requires Redis. If your infrastructure uses database or SQS queues and migrating to Redis isn’t feasible, Supervisor alone is your only option. For new Laravel 12 projects in 2026, starting with Redis is strongly recommended regardless of current scale.
Common Production Pitfalls When Migrating to Horizon
Having migrated several production Laravel applications from plain Supervisor to Horizon, these issues recur consistently:
Forgetting to disable old Supervisor configs. After installing Horizon, you must remove or comment out your previous laravel-worker Supervisor program. Running both causes duplicate job processing. Always verify with supervisorctl status after migration.
Insufficient Redis memory. Horizon stores metrics, tags, and recent job payloads in Redis. A busy application can consume 500 MB+ beyond queue data. Set maxmemory and maxmemory-policy allkeys-lru in Redis config to prevent OOM crashes. On a 2 GB Redis instance, allocate at least 1 GB for Horizon metadata.
Misconfigured maxTime/maxJobs. Without these, Horizon workers run indefinitely. Memory fragmentation in PHP 8.4 accumulates silently over days. Always set maxTime=3600 and maxJobs=1000 as starting points, adjusting based on your job profiles.
Ignoring Horizon’s own Supervisor config. Horizon ships a recommended Supervisor template. Use it. The critical line is command=php /path/to/artisan horizon, not queue:work. Horizon’s master process manages child workers internally.
For teams managing CI/CD pipelines with automated deployments, integrate php artisan horizon:terminate into your deploy script’s post-release hook. This ensures zero-downtime deploys without orphaned workers processing stale code.
Making the Right Choice for Your Laravel Application
The verdict on Laravel Horizon vs Supervisor for queue workers is layered, not binary. Supervisor is non-negotiable infrastructure for any production Laravel queue system. Horizon is the operational upgrade you adopt when visibility, auto-scaling, and debugging efficiency justify the Redis dependency and configuration overhead. For new Laravel 12 projects targeting growth, install Horizon from day one. For existing stable systems processing modest volumes, plain Supervisor remains a valid, low-maintenance choice.
If you’re evaluating queue architecture for a Laravel application and need practical guidance tailored to your traffic patterns, infrastructure constraints, or Nepal-specific deployment considerations, reach out to discuss your project. I’ve configured both approaches across dozens of production systems and can help you avoid the pitfalls that only surface under real load.

