Boring Observability GitHub

Laravel Horizon config checker

Paste your config/horizon.php and config/queue.php. The checker resolves each environment's supervisors the way Horizon does and flags the combinations that fail without an error, like a worker timeout that outlives its connection's retry_after.

It runs in your browser. Nothing you paste is sent anywhere, and no PHP is executed.

Check a config

Try it with:
env() values assumed

The checker can't see your .env, so each env() call uses the default written in it. Type a value to use instead.

    What it checks#

    Each check is a behaviour one of our posts or docs pages works through in detail, and each finding links to that explanation. A setting with no published behaviour behind it, such as memory sizing or nice, is not checked.

    Supervisor timeout at or above the connection's retry_after#

    Problem. retry_after is a lease, not a delay. Once it expires, the next worker to pop the queue takes the job even while the first is still running it. Equal values fail too, because the lease starts at pop() and the timeout alarm a few steps later. With tries => 1 the second copy is failed with MaxAttemptsExceededException; with more tries, both copies run.

    Source: Laravel queue timeout vs retry_after

    A Redis or database connection with no retry_after key#

    Warning. The published config/queue.php says 90. A connection written by hand without the key gets 60 with no warning, which equals the default worker timeout.

    Source: The two numbers and their defaults

    retry_after set to null on a Redis connection#

    Problem. Null skips reserved-job migration altogether. Duplicates stop, and a job whose worker dies is never recovered, with no error to show for it.

    Source: Four ways a correct config becomes an incorrect one

    retry_after on an SQS connection#

    Note. The SQS connector never reads the key. The lease is the queue's Default Visibility Timeout, set on the AWS side and 30 seconds by default, which is half of Laravel's default worker timeout.

    Source: What retry_after does

    A supervisor timeout of 0#

    Warning. pcntl_alarm(0) cancels the alarm rather than firing it, so zero means no timeout. Nothing bounds the job's runtime while the lease keeps expiring on schedule.

    Source: What timeout does

    A supervisor with no tries key#

    Note. The published config sets tries => 1. Drop the key and horizon:supervisor runs with --tries=0, which means unlimited attempts for every job class that does not set its own.

    Source: With tries => 1: a failure on work that succeeded

    Several queues on one unbalanced pool#

    Warning. Without balancing (balance => false, or no balance key at all) the queue list is a strict left-to-right priority. A worker looks at the second queue only when the first is empty, so a flood on a higher queue starves every queue below it until it drains.

    Source: Option B: balance => false and starvation by list order

    An auto-balanced floor that eats the process ceiling#

    Warning. The auto balancer never takes a queue below minProcesses. One worker per queue across thirty queues is a resident floor of thirty, and against maxProcesses 40 it leaves ten processes for the queue that is actually busy.

    Source: Option A: balance => 'auto' and the idle-worker floor

    queueWeights on a balanced supervisor, or a weight of zero#

    Warning. Weights order the queues one shared pool polls, so they mean nothing once each queue has its own pool. Skyline refuses to provision the combination; stock Horizon does not know the key. A zero or negative weight falls back to 1.

    Source: Weighted queues: only with balance => false

    after_commit off on a connection Horizon works#

    Warning. Every connection in the published config/queue.php ships with after_commit => false, so a job dispatched inside DB::transaction() can reach a worker before the row it needs is committed. With no transaction open the job is pushed at once, as before.

    Source: Laravel dispatch after commit

    fast_termination on#

    Note. With the flag on, Horizon exits within about two seconds of being signalled while its workers are still finishing jobs. When supervisord itself is restarted under systemd, as a package upgrade did in our postmortem, those workers keep draining the queues with nothing above them. The fix is a systemd drop-in and a scheduled horizon:purge. Turning the flag off is only safe once the unit is fixed.

    Source: Postmortem: why fast_termination stays on

    A waits threshold of 0#

    Note. Zero turns LongWaitDetected off for that queue, which is sometimes what you want for a housekeeping queue and worth knowing either way.

    Source: What Horizon sends on its own

    A waits key that matches no supervisor#

    Warning. Horizon keys a wait by connection and queue the way a supervisor works them. A pool working high,default is the single entry redis:high,default, so a threshold written as redis:high never applies and the pool gets the 60-second default.

    Source: What Horizon sends on its own

    trim.recent_failed below a week#

    Note. The dashboard's failed-jobs figure counts failures newer than this many minutes. Lowering it, for example to make a Prometheus gauge mean "the past hour", changes the dashboard number too.

    Source: The rule that does not port

    Questions#

    Does my config leave the browser?

    No. The page reads both files with a small parser that runs in your browser, and nothing you paste is sent anywhere. It does not evaluate PHP either: anything other than arrays, literals, casts and env() calls is treated as unknown, and a rule that needs an unknown value says it skipped rather than guessing.

    What does it do with env() calls?

    It uses the default written in the call, because it cannot see your .env. Every env() it met is listed with the value it assumed, and you can type the real value next to any of them to re-run the checks with it.

    What can it not see?

    Anything set on a job class. A $timeout property or #[Timeout] attribute takes precedence over the supervisor's timeout, so a job can break the timeout and retry_after ordering on its own. It also cannot see an SQS queue's visibility timeout, which lives on the AWS side, or your systemd or supervisord unit.

    Which supervisors does it check?

    The ones the chosen environment starts, resolved the way Horizon's provisioning plan does it: the first environments key that matches (keys can be patterns), merged recursively over every supervisor in defaults. A supervisor that appears only in defaults still runs, and one with maxProcesses 0 does not.

    The first check, run by Horizon itself

    Skyline is a drop-in replacement for Laravel Horizon. It warns at startup about any supervisor whose timeout is not below its connection's retry_after, and its misconfiguration alert raises the same thing while it runs.

    See Skyline