Boring Observability GitHub

See the jobs Horizon hides while they wait out a backoff

A Retries page for the Horizon dashboard. It lists the jobs waiting out a backoff or a delay, which Horizon does not show, with a button to run one now.

$ composer require boring-o11y/horizon-delayed-jobs

boring-o11y/horizon-delayed-jobs · MIT · PHP 8.1+, Laravel 10 to 12, Horizon 5.24+

Horizon lists pending, completed and failed jobs. A job released with a five-minute backoff is none of those, so for five minutes it is nowhere in the dashboard. The same goes for anything dispatched with delay() or later().

That gap shows up at the worst moment. A payment provider starts returning 503s, your webhook jobs release themselves with a backoff, and Horizon's pending count stays flat while hundreds of jobs sit waiting. You cannot see how many there are, when they will come back, or push one through once the provider has recovered.

A Retries page in Horizon's own sidebar

The Retries page inside the Laravel Horizon dashboard, listing released jobs with their queue, attempt count, time until they run and a Run now button.

After composer require, a Retries link appears under Failed Jobs. The page lists every job waiting in a queue's delayed set, with its queue, its attempt count and how long until a worker can take it. Tabs split jobs released for a retry from jobs that were scheduled for later, and you can filter by queue or job name.

Run now ends the wait early. The job goes onto the back of its ready queue, exactly as it would when its backoff expired. The payload is not touched, so the job keeps its attempt count and its retryUntil. You can select several jobs and run them together.

It reads the queue, so it cannot drift

Laravel already keeps delayed jobs in a Redis sorted set per queue, scored by the time each one becomes available. The page reads that set directly. There is no listener on dispatch, no extra table and nothing written on the worker path, which is why installing it cannot slow your jobs down.

Running a job early is a single Lua script. If two people press the button at once, or a worker migrates the job in the same instant, only one of them moves it. A job whose backoff expired while the page was open comes back as "no longer delayed" rather than a success that did nothing.

What it does not do

  • It does not show why a job failed last time. That exception lives in Horizon's job record, so look the job id up in Horizon's own job view.
  • It only covers queues a Horizon supervisor processes, from the running supervisors and from config/horizon.php. A queue nothing works on can be added by name in the config.

Horizon's dashboard is a compiled Vue bundle with no extension points, so the package splices its page into Horizon's layout at three anchors. If a Horizon release changes one of them, that piece is skipped with a warning and the rest of the dashboard keeps working. We wrote up how the layout override works separately. Set perform_now to false if you want the page read-only.

Every configuration key, and how to run the test suite, is in the README on GitHub. Bugs and questions go in its issues.

Our other packages

  • httptheus

    Prometheus metrics for your application's outbound HTTP, with a Grafana dashboard to read them. Duration, host, endpoint and outcome of every transfer, and no bodies, headers or database.

  • wirestan

    PHPStan rules for Livewire. A public property the browser can set but the server trusts, like $tenantId seeded in mount(), fails CI until it is marked #[Locked].

  • Unique Job Middleware

    Checks a ShouldBeUnique job's lock again when a worker picks it up, and skips the job if another one holds it. Fires an event for each skip, so you can count them.