# Horizon Delayed Jobs

> A free Retries page for the Laravel Horizon dashboard: every released and delayed job, when it runs next, and a button to run it now.

Source: https://boring-observability.dev/open-source/horizon-delayed-jobs
Install: `composer require boring-o11y/horizon-delayed-jobs`
Requires: PHP 8.1+, Laravel 10 to 12, Horizon 5.24+
Code and full documentation: https://github.com/boring-o11y/horizon-delayed-jobs
License: MIT

---

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

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](https://boring-observability.dev/blog/extending-the-horizon-dashboard) separately. Set `perform_now` to `false` if you want the page read-only.

