Skip to content

Laravel Horizon: Queue Metrics, Supervisor Tuning, and Safe Deployments

Beyond the dashboard: how to use Horizon's metrics API, tune supervisor processes for mixed workloads, and deploy without dropping jobs in production.

Published · Updated · 3 min read

Laravel Horizon: Queue Metrics, Supervisor Tuning, and Safe Deployments

Laravel Horizon in Production: Beyond the Pretty Dashboard

Horizon ships with a slick UI, but the real value for a senior engineer is in its programmatic metrics API, fine-grained supervisor configuration, and the ability to drain workers cleanly during deployments. This article focuses on those three areas with concrete examples.


Querying the Metrics API

Horizon stores throughput and runtime snapshots in Redis. You can read them directly via the Horizon\Contracts\MetricsRepository binding rather than scraping the UI.

use Laravel\Horizon\Contracts\MetricsRepository;

$metrics = app(MetricsRepository::class);

// Throughput (jobs/min) for the last recorded snapshot
$throughput = $metrics->throughput();

// Runtime in milliseconds for a specific job class
$runtime = $metrics->runtimeForJob(ProcessInvoice::class);

// All measured queues
$queues = $metrics->measuredQueues();

This is useful for custom alerting: pipe $throughput into a Prometheus pushgateway or a Slack webhook when it drops below a threshold, without depending on a third-party APM.


Supervisor Configuration for Mixed Workloads

A single supervisor with a high maxProcesses count is the most common Horizon misconfiguration. Different job classes have wildly different runtimes and resource profiles. Separate them.

// config/horizon.php
'environments' => [
    'production' => [
        'supervisor-critical' => [
            'connection' => 'redis',
            'queue'      => ['critical'],
            'balance'    => 'auto',
            'minProcesses' => 2,
            'maxProcesses' => 10,
            'tries'      => 3,
            'timeout'    => 30,
        ],
        'supervisor-default' => [
            'connection' => 'redis',
            'queue'      => ['default'],
            'balance'    => 'simple',
            'processes'  => 4,
            'tries'      => 3,
            'timeout'    => 90,
        ],
        'supervisor-heavy' => [
            'connection' => 'redis',
            'queue'      => ['exports', 'reports'],
            'balance'    => 'simple',
            'processes'  => 2,
            'tries'      => 1,
            'timeout'    => 600,
        ],
    ],
],

Balance Strategies

  • simple — distributes processes evenly across queues in the list. Good when queue depths are predictable.
  • auto — scales processes toward queues with the longest wait time. Use this for critical where latency matters.
  • false — no balancing; each process works the queues in order. Rarely correct in production.

Set balanceCooldown (seconds) when using auto to prevent thrashing:

'balanceCooldown' => 3,
'minProcesses'    => 1,
'maxProcesses'    => 15,

Safe Deployments Without Dropping Jobs

The naive php artisan horizon:terminate in a deploy script kills workers immediately, potentially mid-job. The correct sequence:

# 1. Tell Horizon to finish current jobs then exit
php artisan horizon:pause

# 2. Run your deployment steps (composer, migrations, asset build)
# ...

# 3. Restart Horizon — supervisor (systemd/Forge) brings it back
php artisan horizon:terminate

horizon:pause stops workers from picking up new jobs but lets in-flight jobs complete. horizon:terminate then sends SIGTERM to the master process, which waits for workers to finish before exiting.

Systemd Unit Example

[Unit]
Description=Laravel Horizon
After=network.target

[Service]
User=forge
ExecStart=/usr/bin/php /var/www/app/artisan horizon
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=3600

[Install]
WantedBy=multi-user.target

Setting TimeoutStopSec=3600 gives long-running export jobs up to an hour to finish before systemd force-kills the process. Tune this to your longest expected job runtime.

Horizon in Docker / Kubernetes

In a container environment, trap SIGTERM and delegate to Horizon's own signal handler:

CMD ["php", "artisan", "horizon"]

Kubernetes sends SIGTERM on pod termination. Horizon's master process catches it and begins a graceful shutdown. Set terminationGracePeriodSeconds in your pod spec to match TimeoutStopSec.


Takeaways

  • Use MetricsRepository directly for custom alerting instead of polling the UI.
  • Separate supervisors by job profile (latency-sensitive, long-running, bulk) rather than one fat supervisor.
  • Use balance => auto with a cooldown for variable-load queues; use simple for predictable ones.
  • Always horizon:pause before deploying, then horizon:terminate after assets are in place.
  • Match TimeoutStopSec / terminationGracePeriodSeconds to your longest job timeout to avoid mid-job kills.

Frequently asked questions

What is the difference between `horizon:terminate` and `horizon:pause`?

`horizon:pause` stops workers from pulling new jobs but lets current jobs finish. `horizon:terminate` sends SIGTERM to the Horizon master process, which then waits for all workers to complete before exiting. Use pause first during deploys, then terminate after your code is in place.

When should I use `balance => auto` vs `balance => simple`?

Use `auto` when queue depths fluctuate and you want Horizon to shift workers toward the busiest queue automatically. Use `simple` when queue depths are predictable and you want an even split. Always set `balanceCooldown` with `auto` to prevent rapid process thrashing.

How do I expose Horizon metrics to an external monitoring system?

Inject `Laravel\Horizon\Contracts\MetricsRepository` and call `throughput()` or `runtimeForJob()` inside a scheduled command. From there you can push values to Prometheus, Datadog, or any custom webhook without relying on the Horizon UI or a third-party package.

Mohamed Said

About the author

Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

About GitHub LinkedIn WhatsApp Email Address My CV

Have a technical challenge?

Tell me what you’re building. I reply within two working days.

Discuss your project