PHPackages                             ashiqfardus/horizon-running-jobs - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. ashiqfardus/horizon-running-jobs

ActiveLibrary[Logging &amp; Monitoring](/categories/logging)

ashiqfardus/horizon-running-jobs
================================

Monitor currently running jobs in Laravel Horizon for distributed systems

v2.1.1(2mo ago)724MITPHPPHP ^8.1 || ^8.2 || ^8.3 || ^8.4

Since Jan 7Pushed 2mo agoCompare

[ Source](https://github.com/ashiqfardus/laravel-horizon-running-jobs)[ Packagist](https://packagist.org/packages/ashiqfardus/horizon-running-jobs)[ RSS](/packages/ashiqfardus-horizon-running-jobs/feed)WikiDiscussions master Synced today

READMEChangelog (4)Dependencies (12)Versions (5)Used By (0)

Laravel Horizon Running Jobs
============================

[](#laravel-horizon-running-jobs)

[![Latest Version on Packagist](https://camo.githubusercontent.com/583fcccfc2b41a213ea90199715e3e514534c1012f168bfaf6c5ea6aa7bb4903/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f61736869716661726475732f686f72697a6f6e2d72756e6e696e672d6a6f62732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ashiqfardus/horizon-running-jobs)[![Total Downloads](https://camo.githubusercontent.com/22bfd908a5bcce1a624a5dd2110c274744f3a31be5492161a933bb946528f3af/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f61736869716661726475732f686f72697a6f6e2d72756e6e696e672d6a6f62732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ashiqfardus/horizon-running-jobs)[![License](https://camo.githubusercontent.com/1dee40fe5c936cc874e27762dd61aa13dc23947b483f4172f40350cc3bd121f7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f61736869716661726475732f686f72697a6f6e2d72756e6e696e672d6a6f62732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ashiqfardus/horizon-running-jobs)

**See what's currently running in Laravel Horizon — across one server or many — and act on stuck jobs without leaving the dashboard.**

Horizon's own UI shows pending, completed, and failed jobs but treats "running" as a black box. This package fills that gap with a Blade dashboard, a CLI suite, and an HTTP API. It works on a single application or on any number of instances sharing a Redis. Reserved jobs whose worker died (orphans), reservations that expired without cleanup (zombies), and supervisors that have stopped heartbeating (stale) are all surfaced and recoverable.

---

Table of contents
-----------------

[](#table-of-contents)

- [What you get](#what-you-get)
- [Requirements](#requirements)
- [Installation](#installation)
- [Setup](#setup)
- [Concepts: running, zombie, orphan, stale](#concepts-running-zombie-orphan-stale)
- [Browser dashboard](#browser-dashboard)
- [CLI commands](#cli-commands)
- [HTTP API](#http-api)
- [Securing access](#securing-access)
- [Configuration reference](#configuration-reference)
- [Using the facade](#using-the-facade)
- [How it works internally](#how-it-works-internally)
- [Upgrading from v1.0](#upgrading-from-v10)
- [Testing](#testing)
- [Contributing](#contributing)

---

What you get
------------

[](#what-you-get)

SurfaceWhat it shows / does**`/horizon/queue-monitor`**Standalone Blade dashboard — health banner, supervisor table, queue depths, running jobs with orphan/zombie badges, inline release buttons. Auto-refreshes.**``**Five composable Blade components (`dashboard`, `diagnose-banner`, `supervisors-panel`, `queues-panel`, `running-jobs-table`) you can drop into your own admin pages.**`horizon:running-jobs`**List currently-running jobs. Filter by queue, server, orphan-only. `--watch` for live refresh. `--stats` for aggregates.**`horizon:queues`**Per-queue depth: pending / reserved / delayed / total.**`horizon:supervisors`**Every Horizon supervisor + master process across the deployment, with stale flagging.**`horizon:diagnose`**One-shot health check across supervisors, jobs, and queue depths. Exits non-zero on failure — drop straight into cron.**`horizon:release`**Recover stuck reserved jobs by ID, or all orphans / zombies. Atomic. Confirms before applying.**`GET /api/horizon/*`**JSON endpoints for everything above (auth-gated, throttled).---

Requirements
------------

[](#requirements)

ComponentVersionsPHP8.1, 8.2, 8.3, 8.4Laravel9.x, 10.x, 11.x, 12.x, 13.xHorizon5.x, 6.xRedis6.0+Composer resolves the right Laravel version for your PHP automatically. Laravel 13 requires PHP 8.3+; Laravel 11/12 require PHP 8.2+.

---

Installation
------------

[](#installation)

```
composer require ashiqfardus/horizon-running-jobs
```

Publish the config (optional — defaults are fine for most apps):

```
php artisan vendor:publish --tag=horizon-running-jobs-config
```

That's it. The dashboard at `/horizon/queue-monitor`, all CLI commands, and the HTTP API are wired up automatically. In a `local` or `testing` environment they're open; in any other environment they require an [auth callback](#securing-access) before responding.

### Optional publishables

[](#optional-publishables)

```
# Fork the Blade views to customize markup / structure
php artisan vendor:publish --tag=horizon-running-jobs-views

# Publish the CSS to serve from your own public directory
php artisan vendor:publish --tag=horizon-running-jobs-css
```

---

Setup
-----

[](#setup)

### Single server (default)

[](#single-server-default)

Nothing to configure. The package reads the Redis connection Horizon is using and surfaces everything reserved, regardless of which worker reserved it.

### Distributed (multiple instances sharing one Redis)

[](#distributed-multiple-instances-sharing-one-redis)

Set `distributed => true` in `config/horizon-running-jobs.php`. Each instance will only see jobs reserved by its own Horizon supervisor; pass `--all` (CLI) or `?all=true` (HTTP) to see everything across the cluster.

This applies to any topology where more than one Laravel instance points at the same Redis: multiple machines, containers / pods, even multiple instances on a single host. The discriminator is *shared Redis*, not *multiple servers*.

How the package identifies "this instance" depends on your `config/horizon.php`:

**Auto-detect (works out of the box if your supervisor key is `gethostname()`):**

```
// config/horizon.php
'defaults' => [
    gethostname() => [
        'connection' => 'redis',
        'queue' => ['default'],
    ],
],
```

**Static names (containers, multi-tenant, anywhere `gethostname()` isn't unique):**

```
// config/horizon.php
'defaults' => [
    'supervisor-01' => [...],
    'supervisor-02' => [...],
],
```

```
// config/horizon-running-jobs.php
'server_identifier' => env('HORIZON_SUPERVISOR_NAME'),
```

```
# In each instance's .env
HORIZON_SUPERVISOR_NAME=supervisor-01
```

### Optional: `TracksServer` trait

[](#optional-tracksserver-trait)

If your jobs don't already tag themselves with `server:` via Horizon's `tags()` method, add the trait so the package can match running jobs to the supervisor that reserved them:

```
use Ashiqfardus\HorizonRunningJobs\Traits\TracksServer;

class ProcessOrder implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    use TracksServer;

    public function __construct()
    {
        $this->initializeServerTracking();
    }
}
```

The trait is purely additive — it doesn't replace `tags()` if you already have one.

---

Concepts: running, zombie, orphan, stale
----------------------------------------

[](#concepts-running-zombie-orphan-stale)

Four states. Read these once and the dashboard makes immediate sense.

### `running` — normal

[](#running--normal)

A job is in `queues::reserved` (Redis sorted set), its expiry score is in the future, and the supervisor that reserved it is alive and heartbeating. The worker is processing it.

### `zombie` — reservation expired

[](#zombie--reservation-expired)

The reserved-set entry's score is in the past. The reservation timed out without the worker either completing the job or releasing it cleanly. Causes:

- The worker process died (OOM, SIGKILL) before finishing
- The job exceeded the queue's `retry_after` window
- Horizon hasn't reaped the entry yet

A zombie blocks its queue slot until something releases or removes it.

### `orphan` — worker is gone

[](#orphan--worker-is-gone)

The job's `server:` tag refers to a supervisor that's not in Horizon's live supervisor set anymore. The worker that started this job is gone. The job is structurally stuck — no one will finish it.

A job can be both zombie *and* orphan (`⚠ orphan + zombie`).

### `stale` — supervisor

[](#stale--supervisor)

Different layer. A *supervisor* (worker process manager) entry exists in Redis but its heartbeat expiry is in the past beyond a small grace window. Could mean the supervisor process died, the master stopped pinging it, or there's general Redis lag. A stale supervisor *causes* orphans on jobs it reserved.

### Relationships

[](#relationships)

```
                      Healthy ──────────────────────────┐
                         │                              │
                         ▼                              ▼
   Worker dies ── creates ▶ zombie       Supervisor dies ── creates ▶ stale
                                                                        │
                                                            ▼
                                          Jobs reserved by it become ▶ orphan

```

Recovery: zombies and orphans are recovered by **releasing** them — moving them back to the pending list so a healthy worker can pick them up. Use the dashboard's inline release button or the CLI command `horizon:release`.

---

Browser dashboard
-----------------

[](#browser-dashboard)

### Standalone page

[](#standalone-page)

Visit `/horizon/queue-monitor` in your app. Auth is gated by the same callback as the JSON API ([see below](#securing-access)).

Layout:

- **Top banner** — overall health (PASS / WARN / FAIL) plus per-check findings as a stacked list
- **Supervisors panel** — every supervisor in Horizon's registry, with status, PID, queues, expiry
- **Queue depth panel** — pending / reserved / delayed counts per queue
- **Running jobs table** — orphan and zombie badges (with icon prefixes for color-blind a11y), inline `release` button on rows that need attention, and a "release all" bulk action when ≥1 orphan or zombie is present

Interactivity:

- **Click any row** in running jobs to drill down into the full job payload (class, UUID, queue, server, status, timing, attempts, timeout, tags)
- **Pause auto-refresh** per panel — every panel header has a ⏸/▶ toggle so you can read a row without it disappearing on the next poll
- **Custom confirm modal** for release actions (showing job summary) — no browser-native `confirm()` dialog
- **Toast feedback** on every release with success / failure count

Polling: each panel polls a per-component endpoint on its own interval (banner 5s, supervisors 5s, queues 5s, jobs 3s). No page reload needed; tables re-render in place with a brief fade. Honors `prefers-reduced-motion`.

Disable the route entirely:

```
// config/horizon-running-jobs.php
'ui' => [
    'enabled' => false,
],
```

### Embedding panels in your own dashboard

[](#embedding-panels-in-your-own-dashboard)

Each panel is an anonymous Blade component:

```
{{-- Full dashboard --}}

{{-- Or compose individually --}}

{{-- Filtered to orphans only --}}

```

Component props:

ComponentProps`dashboard``:poll` (default 5000), `:jobs-poll` (default 3000)`diagnose-banner``:poll` (default 5000)`supervisors-panel``:poll` (default 5000)`queues-panel``:poll` (default 5000)`running-jobs-table``:poll` (default 3000), `:allow-release` (default true), `:orphaned-only` (default false)Pass `:poll="0"` to disable a panel's auto-refresh.

For panels embedded in *your* page (not the standalone dashboard), the host page must also load:

- The package CSS, served at `/horizon/queue-monitor/assets/css`, or published via `vendor:publish --tag=horizon-running-jobs-css`
- Alpine.js (Laravel's default for Blade interactivity)
- A `` tag if `:allow-release` is enabled — the release POST is CSRF-protected

The factory functions Alpine needs (`hrjPanel`, `hrjReleaseButton`) are inlined in the standalone dashboard. If you're embedding components in your own page and want auto-refresh + release to work, copy the inline `` block from `vendor/ashiqfardus/horizon-running-jobs/resources/views/dashboard.blade.php` into your layout, or include the published JS.

### Theming

[](#theming)

The package CSS is fully scoped under `.hrj` — it cannot leak into your styles. All colors are CSS variables — override any of them in your own stylesheet to retheme:

```
.hrj {
    --hrj-color-pass:   #00b8a9;
    --hrj-color-warn:   #ffae00;
    --hrj-color-fail:   #ff5b5b;
    --hrj-color-orphan: #ff7a45;
    --hrj-color-zombie: #b388ff;
    --hrj-bg:           #ffffff;
    --hrj-text:         #1a1a1a;
    --hrj-border:       #e5e5e5;
    /* ... see resources/css/horizon-running-jobs.css for the full list */
}
```

Dark mode is auto-detected via `prefers-color-scheme`. Force light by adding `class="hrj hrj--light"` on the wrapper.

---

CLI commands
------------

[](#cli-commands)

Every command supports `--json` for scripting and a `-h` help flag.

### `horizon:running-jobs`

[](#horizonrunning-jobs)

List jobs currently in the reserved set.

```
# Default — current server's jobs (or all jobs in non-distributed mode)
php artisan horizon:running-jobs

# All servers across the cluster
php artisan horizon:running-jobs --all

# Specific queues
php artisan horizon:running-jobs --queue=emails --queue=reports

# Limit display
php artisan horizon:running-jobs --limit=50

# Only orphans (worker process is gone)
php artisan horizon:running-jobs --orphaned

# Live-refresh (Ctrl-C to exit)
php artisan horizon:running-jobs --watch
php artisan horizon:running-jobs --watch=5    # custom interval (seconds)

# Aggregate stats
php artisan horizon:running-jobs --stats

# JSON for scripting
php artisan horizon:running-jobs --json
```

Sample output:

```
🔍 Scanning queues: default, emails, reports
📍 Current server: app-server-01

+----------+--------------------+----------+----------------+----------+----------+----------+----------+
| ID       | Job                | Queue    | Server         | Status   | Started  | Duration | Attempts |
+----------+--------------------+----------+----------------+----------+----------+----------+----------+
| 4b5ecc82 | App\Jobs\Process…  | default  | app-server-01  | running  | 14:30:15 | 2m 34s   | 1        |
| 8a2b3c4d | App\Jobs\StuckJob  | reports  | app-server-01  | ⚠ orphan | 14:31:42 | 12m 08s  | 1        |
+----------+--------------------+----------+----------------+----------+----------+----------+----------+
✓ Found 2 running job(s)
⚠️  1 orphan job(s) detected (worker process is no longer registered)

```

### `horizon:queues`

[](#horizonqueues)

Per-queue depth:

```
php artisan horizon:queues
php artisan horizon:queues --queue=emails --queue=reports
php artisan horizon:queues --json
php artisan horizon:queues --watch
```

```
+---------+---------+----------+---------+-------+
| Queue   | Pending | Reserved | Delayed | Total |
+---------+---------+----------+---------+-------+
| default | 12      | 3        | 0       | 15    |
| emails  | 4       | 1        | 2       | 7     |
| reports | 0       | 0        | 0       | 0     |
+---------+---------+----------+---------+-------+
| TOTAL   | 16      | 4        | 2       | 22    |
+---------+---------+----------+---------+-------+

```

Columns:

- **Pending** — jobs in `queues:` (Redis list), waiting to be picked up
- **Reserved** — jobs in `queues::reserved` (sorted set), currently being processed (or stuck)
- **Delayed** — jobs in `queues::delayed` (sorted set), scheduled to fire later
- **Total** — sum of the three

### `horizon:supervisors`

[](#horizonsupervisors)

Every supervisor and master process Horizon has registered in Redis, across the whole deployment:

```
php artisan horizon:supervisors
php artisan horizon:supervisors --masters    # include master table
php artisan horizon:supervisors --json
php artisan horizon:supervisors --watch
```

```
+-----------------------------------+---------+------+------------------------+-------+---------+
| Name                              | Status  | PID  | Queues                 | Procs | Expires |
+-----------------------------------+---------+------+------------------------+-------+---------+
| supervisor-01:app-01.example.com  | running | 8298 | default,emails,reports | 3     | 67s     |
| supervisor-02:app-02.example.com  | running | 4521 | default,emails,reports | 3     | 73s     |
| supervisor-03:app-03.example.com  | ⚠ stale | -    | -                      | 0     | OVERDUE 12s |
+-----------------------------------+---------+------+------------------------+-------+---------+
⚠ 1 supervisor(s) past their expiry — workers may have died without cleanup.

```

The `Expires` column counts down between Horizon's heartbeats. A supervisor flagged `⚠ stale` has been silent for longer than the grace window (default 5s, see [config](#configuration-reference)), suggesting the master process or the supervisor itself has died.

### `horizon:diagnose`

[](#horizondiagnose)

Unified health check. Exits 0 on pass-or-warn, exits non-zero on hard failure (e.g. no live supervisor at all). Drop straight into cron:

```
php artisan horizon:diagnose
php artisan horizon:diagnose --json
```

```
🔍 Horizon Health Diagnosis

  ✓  horizon.supervisors    2 supervisor(s) running
  ⚠  jobs.orphaned          1 orphan job(s) — see `horizon:running-jobs --orphaned`
  ✓  jobs.zombies           0 zombie jobs
  ✓  jobs.malformed         0 malformed entries
  ✓  queues.depths          highest pending: emails (47), totals: pending=58 reserved=4 delayed=2

Status: WARN

```

Checks:

NamePassWarnFail`horizon.supervisors`At least 1 live, none staleSome stale, OR all stale (Horizon master may have died)ZSET empty (Horizon never started or all entries reaped)`jobs.orphaned`0 orphans≥1 orphan—`jobs.zombies`0 zombies≥1 zombie—`jobs.malformed`0 dropped≥1 dropped (see logs)—`queues.depths`always pass (informational)——### `horizon:release`

[](#horizonrelease)

Move stuck reserved jobs back to the pending list. The only mutating command in the suite. Atomic per-job (ZREM from reserved + LPUSH to pending in one Redis transaction).

```
# Release a single job by UUID
php artisan horizon:release abc-123-def-456

# Release every orphaned reservation
php artisan horizon:release --orphaned

# Release every zombie (expired) reservation, scoped to a specific queue
php artisan horizon:release --zombie --queue=reports

# Preview without modifying Redis
php artisan horizon:release --orphaned --dry-run

# Skip the confirmation prompt (for cron / scripts)
php artisan horizon:release --orphaned --force
```

Behavior:

- Released jobs go to the **front** of the pending list (LPUSH) so a worker picks them up promptly
- Each release is logged via `Log::info` with the job UUID, queue, and reason — audit trail for ops
- `--orphaned`, `--zombie`, and a positional UUID are mutually exclusive — pick one targeting mode
- The interactive confirm shows a full table of jobs that will be released before applying
- `--queue=` repeats to scope to specific queues

### `--watch` mode

[](#--watch-mode)

The list-style commands (`horizon:running-jobs`, `horizon:queues`, `horizon:supervisors`) accept a `--watch[=seconds]` flag that re-renders on a loop, like `top`:

```
php artisan horizon:running-jobs --watch       # 3s default
php artisan horizon:queues --watch=10          # 10s interval
```

Press `Ctrl-C` to exit. Ignored when combined with `--json`.

### `--json` mode

[](#--json-mode)

Every command emits machine-readable JSON with `--json`:

```
php artisan horizon:queues --json | jq '.totals.pending'
php artisan horizon:diagnose --json | jq '.overall_status'
```

`horizon:diagnose --json` is particularly useful for monitoring/alerting:

```
if [ "$(php artisan horizon:diagnose --json | jq -r .overall_status)" = "fail" ]; then
    page-oncall "Horizon is down"
fi
```

---

HTTP API
--------

[](#http-api)

Auth-gated by the same callback as the dashboard. Throttled to 60 requests/minute per caller by default.

### `GET /api/horizon/running-jobs`

[](#get-apihorizonrunning-jobs)

```
GET /api/horizon/running-jobs
GET /api/horizon/running-jobs?all=true
GET /api/horizon/running-jobs?queues=emails,reports
GET /api/horizon/running-jobs?orphaned=true
```

Sample response:

```
{
  "success": true,
  "hostname": "app-server-01",
  "timestamp": "2026-04-25T10:30:00+00:00",
  "queues_monitored": ["default", "emails", "reports"],
  "running_jobs_count": 2,
  "total_count": 2,
  "dropped_count": 0,
  "orphan_count": 1,
  "orphaned_only": false,
  "jobs": [
    {
      "job_id": "4b5ecc82-07a7-40db-97db-bfab5ac5c500",
      "job_class": "App\\Jobs\\ProcessOrder",
      "queue": "default",
      "server": "app-server-01",
      "status": "running",
      "is_orphaned": false,
      "start_time": "2026-04-25T10:27:26+00:00",
      "start_timestamp": 1745576846,
      "running_for_seconds": 154,
      "running_for_formatted": "2m 34s",
      "attempts": 1,
      "timeout": 120,
      "tags": ["server:app-server-01", "environment:production"]
    }
  ],
  "warnings": []
}
```

Response field reference:

FieldMeaning`running_jobs_count`jobs returned in this payload (may be limited by `max_jobs`)`total_count`total reserved-set entries found before truncation`dropped_count`malformed reserved-set entries skipped; each is logged via `Log::warning``orphan_count`jobs whose tagged supervisor is no longer in Horizon's live set`orphaned_only`echoes whether `?orphaned=true` was active for this request`jobs[].status``"running"` (reservation valid) or `"zombie"` (reservation expired)`jobs[].is_orphaned``true` when the worker that reserved the job is no longer registered`jobs[].start_time` / `start_timestamp`actual reservation time (not the Redis expiry score)`warnings[]`human-readable summary lines — long-running, zombie count, orphan count, dropped count### `GET /api/horizon/running-jobs/stats`

[](#get-apihorizonrunning-jobsstats)

Aggregate stats:

```
{
  "success": true,
  "timestamp": "2026-04-25T10:30:00+00:00",
  "stats": {
    "total_running": 5,
    "by_server": {"app-01": 3, "app-02": 2},
    "by_queue": {"default": 4, "reports": 1},
    "by_job_class": {"App\\Jobs\\ProcessOrder": 5},
    "by_status": {"running": 4, "zombie": 1},
    "by_orphan_status": {"healthy": 4, "orphaned": 1},
    "dropped_count": 0,
    "orphan_count": 1,
    "longest_running": { /* job object */ },
    "warnings": []
  }
}
```

### `GET /api/horizon/queues`

[](#get-apihorizonqueues)

```
GET /api/horizon/queues
GET /api/horizon/queues?queues=emails,reports
```

```
{
  "success": true,
  "inspected_at": 1745576846,
  "queue_count": 3,
  "totals": {"pending": 16, "reserved": 4, "delayed": 2, "total": 22},
  "queues": [
    {"queue": "default", "pending": 12, "reserved": 3, "delayed": 0, "total": 15},
    {"queue": "emails",  "pending": 4,  "reserved": 1, "delayed": 2, "total": 7},
    {"queue": "reports", "pending": 0,  "reserved": 0, "delayed": 0, "total": 0}
  ]
}
```

### `GET /api/horizon/supervisors`

[](#get-apihorizonsupervisors)

```
{
  "success": true,
  "inspected_at": 1745576846,
  "supervisor_count": 2,
  "master_count": 1,
  "stale_supervisor_count": 0,
  "supervisors": [
    {
      "name": "supervisor-01:app-01.example.com",
      "status": "running",
      "master": "supervisor-01",
      "pid": 8298,
      "queues": ["default", "emails", "reports"],
      "process_count": 3,
      "processes": {"redis:default": 1, "redis:emails": 1, "redis:reports": 1},
      "expires_at": 1745576906,
      "seconds_until_expiry": 60,
      "is_stale": false
    }
  ],
  "masters": [
    {
      "name": "supervisor-01",
      "status": "running",
      "environment": "production",
      "pid": 8283,
      "supervisor_count": 1,
      "expires_at": 1745576901,
      "seconds_until_expiry": 55,
      "is_stale": false
    }
  ]
}
```

### Validation

[](#validation)

Endpoints accepting `?queues=` enforce:

- Each name matches `[A-Za-z0-9_:.-]+`
- At most 20 names per request
- Invalid input → `422 Unprocessable Entity`

---

Securing access
---------------

[](#securing-access)

The package is **safe by default**:

- In `local` and `testing` environments — open. Zero-friction development.
- Anywhere else — denied with a 403 unless you register an auth callback.
- Throttled to 60 requests/minute per caller out of the box.

### Production: register an auth callback

[](#production-register-an-auth-callback)

In your `AppServiceProvider::boot()`:

```
use Ashiqfardus\HorizonRunningJobs\HorizonRunningJobs;

public function boot(): void
{
    HorizonRunningJobs::auth(function ($request) {
        return $request->user()?->is_admin === true;
    });
}
```

The closure receives the incoming `Illuminate\Http\Request`. Return `true` to allow, `false` to deny. Works with whatever auth scheme you have — Sanctum, Passport, sessions, custom.

If you forget to register the callback in production, the 403 response includes a copy-paste example showing exactly how to fix it.

### Layering with auth middleware (optional)

[](#layering-with-auth-middleware-optional)

Add additional middleware to defend in depth:

```
// config/horizon-running-jobs.php
'routes' => [
    'middleware' => ['api', 'throttle:60,1', 'auth:sanctum'],
],
```

The bundled `Authorize` middleware runs *after* whatever you configure here, so you get both — middleware AND callback must pass.

### Disable the routes entirely

[](#disable-the-routes-entirely)

If you'd rather wire your own controllers / Gate-based authorization:

```
'routes' => [
    'enabled' => false,
],
'ui' => [
    'enabled' => false,
],
```

---

Configuration reference
-----------------------

[](#configuration-reference)

Every config key, with default and meaning. From `config/horizon-running-jobs.php`:

KeyDefaultMeaning`distributed``false`Enable when more than one Laravel instance shares one Redis. Each instance only sees its own jobs unless `--all` / `?all=true` is passed.`server_identifier``null`How this instance identifies itself in distributed mode. `null` = auto-detect from `gethostname()`. Override for static names, containers, etc.`queues``null`Default queues to monitor. `null` = auto-detect from `config('horizon.defaults.*.queue')`. Pass `['default', 'emails']` to pin.`max_jobs``1000`Hard cap on jobs returned in a single query. Prevents memory blowups on very deep reserved sets.`long_running_threshold``300`Seconds before a job's row is flagged as "long-running" in warnings.`cache.enabled``true`Cache HTTP API responses for `cache.ttl` seconds.`cache.ttl``10`Cache duration in seconds.`cache.prefix``'horizon_running_jobs'`Cache key prefix.`routes.enabled``true`Whether the JSON API routes are registered.`routes.prefix``'api'`URL prefix for API routes.`routes.middleware``['api', 'throttle:60,1']`Middleware stack for API routes. The `Authorize` middleware is appended unconditionally.`routes.uri``'horizon/running-jobs'`Path segment for the running-jobs endpoint.`ui.enabled``true`Whether the Blade dashboard route is registered.`ui.prefix``'horizon/queue-monitor'`URL prefix for the dashboard.`ui.middleware``['web']`Middleware stack for the dashboard. The `Authorize` middleware is appended unconditionally. `web` is required for sessions + CSRF on the release POST.`redis_connection``null`Redis connection name. `null` = auto-detect from `config('horizon.use')`.`retry_after``null`Override Horizon's `retry_after` window for duration math. `null` = auto-detect from `config('queue.connections..retry_after')`, falling back to 90.`supervisor_stale_grace_seconds``5`Grace window before flagging a supervisor stale. Absorbs heartbeat jitter. Lower = more responsive but flappier; higher = stabler but slower outage detection.---

Using the facade
----------------

[](#using-the-facade)

```
use Ashiqfardus\HorizonRunningJobs\Facades\RunningJobs;

// Current server only
$result = RunningJobs::getRunningJobs();

// All servers
$result = RunningJobs::getRunningJobs(null, true);

// Specific queues
$result = RunningJobs::getRunningJobs(null, false, ['emails', 'reports']);

// Filter to orphans only
$result = RunningJobs::getRunningJobs(null, true, null, $orphanedOnly = true);

// Aggregate stats
$stats = RunningJobs::getStats();
```

For releasing jobs programmatically:

```
use Ashiqfardus\HorizonRunningJobs\JobReleaser;

$releaser = app(JobReleaser::class);

// Find what's releasable (read-only)
$found = $releaser->findReleasable(['orphaned' => true, 'queues' => ['reports']]);

// Release them (atomic per-job)
$count = $releaser->release($found);
```

---

How it works internally
-----------------------

[](#how-it-works-internally)

Laravel's Redis queue stores jobs in three keys per queue:

KeyTypeContains`queues:{q}`LISTPending jobs (workers `LPOP` from here)`queues:{q}:reserved`ZSETCurrently-reserved jobs (score = expiry timestamp)`queues:{q}:delayed`ZSETScheduled / delayed jobs (score = release timestamp)This package reads all three. For supervisors / health, it also reads Horizon's own keys on the `horizon` Redis connection:

KeyTypeContains`supervisors`ZSETLive supervisor names (score = expiry)`masters`ZSETLive master process names (score = expiry)`supervisor:{name}`HASHPer-supervisor metadata (pid, queues, process counts)`master:{name}`HASHPer-master metadata### Identifying which job belongs to which server

[](#identifying-which-job-belongs-to-which-server)

Two paths, in order:

1. **Tags** — Horizon stores tags as part of the job payload. The package looks for `server:` and matches against the supervisor name.
2. **`supervisor_id` property** — fallback if no tag is set. The package extracts via regex (no `unserialize`).

This is why the `TracksServer` trait is a quality-of-life affordance — it adds the tag automatically. If you have your own `tags()` returning `server:gethostname()`, the package picks that up too.

### Distributed mode

[](#distributed-mode)

```
                    ┌─────────────────┐
                    │  Redis Server   │
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
         ┌────▼────┐    ┌────▼────┐   ┌────▼────┐
         │ App A   │    │ App B   │   │ App C   │
         │ 5 jobs  │    │ 3 jobs  │   │ 7 jobs  │
         └─────────┘    └─────────┘   └─────────┘

```

Each instance can see its own jobs (default) or all jobs in the cluster (with `--all` / `?all=true`).

### Cache invalidation

[](#cache-invalidation)

API responses cache for `cache.ttl` seconds (default 10) to prevent hammering Redis. Cache keys embed an epoch counter; calling `RunningJobs::clearCache()` increments the epoch, invalidating every previously-cached response without needing wildcard deletes.

---

Upgrading from v1.0
-------------------

[](#upgrading-from-v10)

v2.0 introduces several breaking changes plus a substantial feature set. Before upgrading:

### Breaking changes

[](#breaking-changes)

1. **Production endpoints now deny by default.** v1.0 left the routes wide open in any environment. v2.0 returns `403` from `local`/`testing`-other environments unless you register an auth callback. The 403 body includes a copy-paste example. See [Securing access](#securing-access).
2. **`RunningJobsManager::parseJobData()` throws `RuntimeException`** on malformed payloads (was: returned `null`). Calling code that relied on the null return needs to wrap in try/catch or call from inside the manager which already handles it.
3. **`response().jobs[*].start_time` / `start_timestamp` reflect the actual reservation time** rather than the Redis expiry score. Charts that built duration math on the v1 values may need adjustment.
4. **Default route middleware adds `throttle:60,1`.** Callers exceeding 60 requests/minute now receive 429.
5. **PHP 8.0 dropped.** Minimum is PHP 8.1.

### Additive (no action needed)

[](#additive-no-action-needed)

- New CLI commands: `horizon:queues`, `horizon:supervisors`, `horizon:diagnose`, `horizon:release`
- New HTTP endpoints: `/api/horizon/queues`, `/api/horizon/supervisors`
- New Blade dashboard at `/horizon/queue-monitor` + composable components
- New job fields: `is_orphaned`, `status` (`"running"` | `"zombie"`)
- New response fields: `orphan_count`, `dropped_count`, `orphaned_only`
- `--watch` flag on list-style commands

### Deprecated (still works, will be removed in v3.0)

[](#deprecated-still-works-will-be-removed-in-v30)

- The standalone JS widget (`vendor:publish --tag=horizon-running-jobs-assets`) and the bundled Vue component. Both only show running jobs and lack any of the v2 features. Migrate to the Blade dashboard.

---

Testing
-------

[](#testing)

```
composer test
```

Runs the full PHPUnit suite. No Redis required for unit + feature tests; integration tests skip themselves when Redis isn't reachable on `127.0.0.1:6379`.

For end-to-end testing against a real Laravel app, there's a companion demo project at **[github.com/ashiqfardus/laravel-horizon-running-jobs-demo](https://github.com/ashiqfardus/laravel-horizon-running-jobs-demo)** — a fresh Laravel 13 install with Horizon configured and a handful of dummy jobs (fast / medium / slow / flaky / memory-heavy / stuck) already wired up. Clone it as a sibling of this package directory and it'll resolve via Composer's path symlink:

```
git clone https://github.com/ashiqfardus/laravel-horizon-running-jobs-demo.git
cd laravel-horizon-running-jobs-demo
composer install
cp .env.example .env && php artisan key:generate

php artisan horizon                           # terminal 1
php artisan demo:dispatch-workload            # terminal 2 — varied jobs across queues
php artisan demo:simulate-orphan --count=2    # terminal 2 — flip to broken state
php artisan horizon:running-jobs --orphaned   # observe the orphans
php artisan horizon:release --orphaned        # release them back to pending
```

The demo also serves the Blade dashboard at `/horizon/queue-monitor`.

---

Contributing
------------

[](#contributing)

PRs welcome. Run `composer test` before submitting. Integration tests require Redis on `127.0.0.1:6379`.

Security
--------

[](#security)

Found a security issue? Email **** instead of using the public issue tracker.

Credits
-------

[](#credits)

- [Ashiq Fardus](https://github.com/ashiqfardus)
- [All contributors](../../contributors)

License
-------

[](#license)

MIT — see [LICENSE.md](LICENSE.md).

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance87

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~38 days

Total

4

Last Release

65d ago

Major Versions

v1.0.0 → v2.0.02026-04-25

PHP version history (3 changes)v1.0.0PHP ^8.0

v2.0.0PHP ^8.1

v2.1.0PHP ^8.1 || ^8.2 || ^8.3 || ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/ce04a6996fce40ab5d66524ee273f295e7569874dec4b62dd86c528a500ade00?d=identicon)[ashiqfardus](/maintainers/ashiqfardus)

---

Top Contributors

[![ashiqfardus](https://avatars.githubusercontent.com/u/19163772?v=4)](https://github.com/ashiqfardus "ashiqfardus (48 commits)")

---

Tags

horizonlaravelmonitoringqueuelaravelmonitoringredisqueuejobshorizondistributed

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ashiqfardus-horizon-running-jobs/health.svg)

```
[![Health](https://phpackages.com/badges/ashiqfardus-horizon-running-jobs/health.svg)](https://phpackages.com/packages/ashiqfardus-horizon-running-jobs)
```

###  Alternatives

[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M132](/packages/laravel-pulse)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M199](/packages/laravel-ai)[spatie/laravel-health

Monitor the health of a Laravel application

87512.0M166](/packages/spatie-laravel-health)[inspector-apm/inspector-laravel

Code Execution Monitoring, built for developers.

2382.1M5](/packages/inspector-apm-inspector-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
