PHPackages                             kpconnell/laravel-jobwarden - 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. [Queues &amp; Workers](/categories/queues)
4. /
5. kpconnell/laravel-jobwarden

ActiveLibrary[Queues &amp; Workers](/categories/queues)

kpconnell/laravel-jobwarden
===========================

A database-backed alternative to Horizon &amp; Laravel Queues — durable jobs, sophisticated batches, high observability &amp; scheduling that survive worker and host crashes, with no Redis to operate.

v1.0.0-beta(today)00MITPHPPHP ^8.3CI passing

Since Jul 1Pushed todayCompare

[ Source](https://github.com/kpconnell/laravel-jobwarden)[ Packagist](https://packagist.org/packages/kpconnell/laravel-jobwarden)[ Docs](https://github.com/kpconnell/laravel-jobwarden)[ RSS](/packages/kpconnell-laravel-jobwarden/feed)WikiDiscussions main Synced today

READMEChangelog (2)Dependencies (11)Versions (2)Used By (0)

Laravel JobWarden
=================

[](#laravel-jobwarden)

[![Tests](https://github.com/kpconnell/laravel-jobwarden/actions/workflows/tests.yml/badge.svg)](https://github.com/kpconnell/laravel-jobwarden/actions/workflows/tests.yml)[![Latest Version](https://camo.githubusercontent.com/3538a06f5d30b0615f2388837d58fc6c8b7fcaa28d9ccaa0a262d7a5fab0b370/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6b70636f6e6e656c6c2f6c61726176656c2d6a6f6277617264656e2e737667)](https://packagist.org/packages/kpconnell/laravel-jobwarden)[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)

A durable, database-backed **job, batch &amp; scheduling engine** for Laravel — a deliberate alternative to the Redis/Horizon model in which **the relational database is the source of truth and the coordination layer**. Correctness, recovery, and observability come from durable state transitions, per-process fencing tokens, an idempotency-gated retry guard, and verifiable OS-process identity — never from assuming a worker is healthy.

> **Status: `1.0.0-beta`.** The distributed-correctness core and the full feature set are complete and proven against SQLite, MariaDB/MySQL, and PostgreSQL (134 tests). APIs may shift slightly before `1.0.0`.

Why
---

[](#why)

- **Survives worker death.** A worker (or its whole host) can crash mid-job. A reaper detects the dead process by its stale lease, orphans its in-flight work with a bumped fencing token, and re-runs it elsewhere — and every step is recorded in a durable audit ledger.
- **No Redis to operate.** Your database is already durable, transactional, and backed up. JobWarden coordinates entirely through it (`FOR UPDATE SKIP LOCKED`, with an optimistic-CAS fallback where that isn't available).
- **Idempotency is a first-class, binary decision.** Each job declares `idempotent()`. Lost idempotent jobs retry automatically; non-idempotent ones **park** for an operator instead of silently double-running.
- **Batches, DAGs, and scheduling** are built in — fan-out, chains, arbitrary dependency graphs, cron, and one-off runs — all on the same durable substrate.

JobWarden coexists with Laravel's Bus/Queue; it does **not** hijack `dispatch()`.

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

[](#requirements)

- PHP **8.3+**, Laravel **11 or 12**
- **Linux** for the runtime (the liveness model uses POSIX signals, `proc_open`/`pcntl`, and `/proc`)
- A database — best with `SKIP LOCKED` (PostgreSQL ≥ 9.5, MySQL ≥ 8.0.1, MariaDB ≥ 10.6); others fall back to an optimistic claim. MariaDB on RDS is the primary production target.

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

[](#installation)

```
composer require kpconnell/laravel-jobwarden
php artisan jobwarden:install --migrate
```

`jobwarden:install` publishes `config/jobwarden.php` and the migrations; `--migrate` runs them. By default JobWarden uses a **dedicated database connection** (`config('jobwarden.connection')`) so its coordination traffic is isolated from your app's.

Defining a job
--------------

[](#defining-a-job)

A JobWarden job implements one small contract — it receives plain, JSON-serializable `params` (not a serialized object graph), and declares whether it is safe to auto-retry:

```
use JobWarden\Contracts\JobWardenJob;
use JobWarden\Runner\JobContext;

final class ImportCatalog implements JobWardenJob
{
    public function handle(JobContext $context): void
    {
        $storeId = $context->params['store_id'];
        // ... do the work; throwing = failure, returning = success ...
    }

    public function idempotent(): bool
    {
        return true; // lost/failed runs may be safely re-executed
    }
}
```

Dispatching
-----------

[](#dispatching)

```
use JobWarden\JobWarden;

app(JobWarden::class)->dispatch(ImportCatalog::class, ['store_id' => 42], [
    'idempotent'   => true,
    'max_attempts' => 3,
    'priority'     => 10,
    'available_at' => now()->addMinutes(5), // optional delay
]);
```

### Batches (fan-out, chains, DAGs)

[](#batches-fan-out-chains-dags)

```
app(JobWarden::class)->batch('nightly-sync', failurePolicy: 'continue')
    ->add('extract',   ExtractJob::class,   ['store_id' => 42])
    ->add('transform', TransformJob::class, ['store_id' => 42], dependsOn: ['extract'])
    ->add('load',      LoadJob::class,      ['store_id' => 42], dependsOn: ['transform'])
    ->add('report',    ReportJob::class,    [],                 dependsOn: ['load'])
    ->dispatch();
```

A member with no `dependsOn` starts immediately; one with dependencies is admitted only when **all** of them have succeeded. Independent chains run in parallel. Failure policies: `continue`, `fail_fast`, `threshold(N)`.

### Scheduling

[](#scheduling)

```
$jw = app(JobWarden::class);
$jw->schedule('hourly-metrics', '0 * * * *', ComputeMetrics::class);      // cron → a job
$jw->scheduleCommand('nightly-prune', '0 3 * * *', 'cache:prune');        // cron → an artisan command
$jw->scheduleOnce('one-off', now()->addHour(), SendDigest::class);        // fire once
```

Running the engine
------------------

[](#running-the-engine)

JobWarden runs as **long-running processes**. `jobwarden:work` already brings its own Tier-2 local reaper (a co-resident child process), so the minimum is a worker, the global reaper, and the scheduler:

```
php artisan jobwarden:work          # claim + run jobs — and bundle a co-resident Tier-2 reaper
php artisan jobwarden:reap:global   # Tier-3: detect dead workers fleet-wide (leader-leased)
php artisan jobwarden:schedule      # evaluate schedules
```

You never have to remember `jobwarden:reap:local` — the worker spawns it, and a per-host lease keeps exactly one active even when several workers share a box. (It remains a standalone command for advanced split topologies.)

Each daemon should be supervised by the OS (systemd `Restart=always`, or a container restart policy). Unit templates are in [`packaging/systemd/`](packaging/systemd), and a container image that runs any set of roles via a `JOBWARDEN_ROLES` env var is in [`docker/`](docker) (see [`docker-compose.yml`](docker-compose.yml) for a full local stack).

**→ See [docs/HOSTING.md](docs/HOSTING.md)** for deployment topologies: serving the UI from your existing app host, running everything on a single worker box, and how to scale out to a fleet.

### How recovery works (the short version)

[](#how-recovery-works-the-short-version)

Every claim is stamped with the claiming worker's globally-unique id and a fencing token. A worker heartbeats a lease while it lives. When a lease goes stale, a reaper orphans that worker's in-flight attempts — bumping the fence so the presumed-dead worker can never clobber the reassignment — and recovery re-queues idempotent jobs or parks non-idempotent ones. Liveness is never the job's responsibility: jobs run in a child process the supervisor watches, so a job that blocks for an hour is never mistaken for a dead one.

Operator API &amp; dashboard
----------------------------

[](#operator-api--dashboard)

A gated JSON API (read models + actions + scheduling) mounts under `config('jobwarden.api.prefix')`, and a server-rendered Livewire dashboard mounts under `config('jobwarden.dashboard.prefix')`. Both sit behind an authorization gate that defaults to local-only — open it explicitly:

```
use JobWarden\JobWarden;

JobWarden::auth(fn ($request) => $request->user()?->can('viewJobWarden') ?? false);
```

See [`docs/API.md`](docs/API.md) for the full endpoint reference.

Testing
-------

[](#testing)

```
composer test                       # SQLite (fast)
# full matrix (SQLite + MariaDB + Postgres) runs in the Docker stack and in CI
docker compose run --rm migrate php vendor/bin/testbench package:test
```

License
-------

[](#license)

MIT © Kevin Connell. See [LICENSE](LICENSE).

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity34

Early-stage or recently created project

 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

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

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

---

Top Contributors

[![kpconnell](https://avatars.githubusercontent.com/u/1077478?v=4)](https://github.com/kpconnell "kpconnell (13 commits)")

---

Tags

schedulerlaravelqueuebatchjobsstate-machineworkersdurableorphan-recovery

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/kpconnell-laravel-jobwarden/health.svg)

```
[![Health](https://phpackages.com/badges/kpconnell-laravel-jobwarden/health.svg)](https://phpackages.com/packages/kpconnell-laravel-jobwarden)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M345](/packages/psalm-plugin-laravel)[laravel/pulse

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

1.7k15.1M129](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M125](/packages/roots-acorn)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

77022.3M143](/packages/laravel-mcp)[illuminate/queue

The Illuminate Queue package.

21332.6M1.5k](/packages/illuminate-queue)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45444.2k1](/packages/pressbooks-pressbooks)

PHPackages © 2026

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