Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b97fbfd
docs(web): add the 4.2 "Running Jobs on Multiple Servers" guide
bpamiri Oct 8, 2026
323e480
docs(web): correct the multi-server jobs guide per accuracy review
bpamiri Oct 8, 2026
30debce
Merge remote-tracking branch 'origin/develop' into peter/4485-multi-s…
bpamiri Oct 8, 2026
ee1c853
docs(web): fold the merged processQueue-through-worker change into th…
bpamiri Oct 8, 2026
e12ffec
docs(web): phrase the heartbeat lease-renewal note as forward-looking
bpamiri Oct 8, 2026
a7d50e4
Merge remote-tracking branch 'origin/develop' into peter/4485-multi-s…
bpamiri Oct 8, 2026
e9eea3e
docs(web): document recurring job schedules (#4493)
bpamiri Oct 8, 2026
99b7d4c
docs(web): mark the planned automatic schedule runner
bpamiri Oct 8, 2026
407f066
Merge remote-tracking branch 'origin/develop' into peter/4485-multi-s…
bpamiri Oct 8, 2026
86790fe
docs(web): note the uniqueKey index on old SQL Server levels (#4494)
bpamiri Oct 8, 2026
57eb8f3
docs(web): correct processQueue's skipped claim and add the fenced count
bpamiri Oct 8, 2026
0e0f04e
Merge remote-tracking branch 'origin/develop' into peter/4485-multi-s…
bpamiri Oct 8, 2026
da38a4d
docs(web): document exclusive jobs and concurrency keys (#4496)
bpamiri Oct 8, 2026
29a4f9a
docs(web): correct the lease window source and soften the renewal wor…
bpamiri Oct 8, 2026
43c6dfe
Merge remote-tracking branch 'origin/develop' into peter/4485-multi-s…
bpamiri Oct 8, 2026
7174b59
docs(web): document job lifecycle hooks and the failure hook (#4498)
bpamiri Oct 8, 2026
e082af1
docs(web): fix jobsOnFailure signature, var scoping, and the result cap
bpamiri Oct 8, 2026
0e8e046
docs(web): clarify the result cap is a byte bound, not VARCHAR byte s…
bpamiri Oct 8, 2026
ae4640a
Merge remote-tracking branch 'origin/develop' into peter/4485-multi-s…
bpamiri Oct 8, 2026
307364c
docs(web): heartbeats shorten dead-worker detection (#4507)
bpamiri Oct 8, 2026
4fc4fac
docs(web): qualify the long timeout and the lease-vs-reclaim windows
bpamiri Oct 8, 2026
2d2a264
docs(web): sharpen the runner cap and lease-window details
bpamiri Oct 8, 2026
8ec5102
docs(web): correct the schedule-runner placeholder default to 15
bpamiri Oct 8, 2026
9e95c65
Merge remote-tracking branch 'origin/develop' into peter/4485-multi-s…
bpamiri Oct 8, 2026
64c2a6f
docs(web): document wheels jobs install, and the fixed worker timeout
bpamiri Oct 8, 2026
8666479
docs(web): tighten the jobs-install schema notes
bpamiri Oct 8, 2026
5930028
docs(web): soften the --timeout worker note to runtime-dependent
bpamiri Oct 8, 2026
8562596
Merge remote-tracking branch 'origin/develop' into peter/4485-multi-s…
bpamiri Oct 9, 2026
4d76f39
docs(web): document the /wheels/jobs/tick route (#4516)
bpamiri Oct 9, 2026
d9de7de
docs(web): schedules run from the worker (#4509); tick-route job limits
bpamiri Oct 9, 2026
746bba4
docs(web): link #4520 from the tick-route one-job-per-call note
bpamiri Oct 9, 2026
bbe4596
Merge remote-tracking branch 'origin/develop' into peter/4485-multi-s…
bpamiri Oct 9, 2026
9a23903
docs(web): job timestamps on the database's UTC clock (#4486)
bpamiri Oct 9, 2026
bdd935f
docs(web): spell out the UTC conversion upgrade path
bpamiri Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ wheels jobs enqueue billing.InvoiceJob --queue=billing --priority=5
| `--at` | Run it at this time, in ISO 8601: with `Z` or an offset (`2026-10-06T11:00:00+02:00`), or without one for this machine's local time (`2026-10-06 09:00`). A time in the past is refused. |
| `--format=json` | Print the enqueued job as JSON. |

"Scheduling" here means one delayed run (`--in` / `--at`, the same as `enqueueIn()` / `enqueueAt()`). Wheels has no recurring, cron-style schedule; for that, run `wheels jobs enqueue` or `processQueue()` from your system's scheduler.
"Scheduling" here means one delayed run (`--in` / `--at`, the same as `enqueueIn()` / `enqueueAt()`). For a *recurring*, cron-style schedule, declare it in `config/schedules.cfm` — see [Recurring schedules](/v4-2-0/digging-deeper/running-jobs-on-multiple-servers/#recurring-schedules).

The class must be a component under `app/jobs/` (or a `jobClassPrefixes` path, the same list the worker accepts) that extends `wheels.Job`. Anything else is refused before it's loaded, and an unknown name lists the job classes it found. Enqueueing writes a row, so like `wheels jobs work` it only talks to this project's own server and sends the reload password.

Expand Down Expand Up @@ -220,10 +220,14 @@ wheels jobs work --quiet # suppress per-job output; failur
wheels jobs work --job-timeout=600 # cap each job's run time at 600 seconds (default: each job's own timeout)
```

Each job runs with its own `timeout` (set in `config()`, default `300` seconds), so a job with `this.timeout = 900` gets its full 15 minutes. `--job-timeout=<seconds>` caps that: a job with a longer `timeout` is cut off at the cap, and one with a shorter `timeout` keeps its own. (Don't use `--timeout`: that is the CLI runtime's own option, which stops the whole worker after that many seconds.) Before 4.2 the worker stopped every job at 300 seconds, whatever its `timeout`.
Each job runs with its own `timeout` (set in `config()`, default `300` seconds), so a job with `this.timeout = 900` gets its full 15 minutes. `--job-timeout=<seconds>` caps that: a job with a longer `timeout` is cut off at the cap, and one with a shorter `timeout` keeps its own. (Use `--job-timeout`, not `--timeout`: on some CLI runtimes `--timeout` is the runtime's own root option and would stop the whole worker after that many seconds.) Before 4.2 the worker stopped every job at 300 seconds, whatever its `timeout`.

In production, run the worker under a process supervisor (systemd, Docker's restart policy, Kamal accessory containers): when it loses the server it exits non-zero and the supervisor restarts it. Run several workers for parallelism — the framework uses optimistic row locking, so two workers never run the same job twice. `--queue` accepts a comma-delimited list (`--queue=critical,default`); jobs across the listed queues run in `priority DESC, runAt ASC` order.

<Aside type="tip" title="Running on more than one server">
When several app servers process the same `wheels_jobs` table, there's more to know — how a crashed or stalled worker's job is recovered safely, keeping a long job alive with `heartbeat()`, capping how many jobs a server runs at once, and draining a server before a deploy. See [Running Jobs on Multiple Servers](/v4-2-0/digging-deeper/running-jobs-on-multiple-servers/).
</Aside>

Check queue health without tailing logs:

```bash title="your shell"
Expand All @@ -243,7 +247,7 @@ component extends="Controller" {
function drain() {
worker = new wheels.Job();
result = worker.processQueue(queue="mailers", limit=10);
// result = {processed: n, failed: n, skipped: n, errors: [...]}
// result = {processed: n, failed: n, skipped: n, fenced: n, leasesLost: n, errors: [...]}
renderWith(data=result);
}
}
Expand All @@ -253,7 +257,7 @@ component extends="Controller" {
- `processQueue(queue="mailers")` — only drain the `mailers` queue.
- `processQueue(limit=100)` — bigger batch for one-shot catch-ups.

In production, schedule the call at whatever cadence matches your latency tolerance — every minute is plenty for emails. Run it from multiple schedulers for parallelism: the base class uses optimistic row locking, so two pollers won't process the same job twice (a row claimed between the SELECT and the claim shows up in the `skipped` count).
In production, schedule the call at whatever cadence matches your latency tolerance — every minute is plenty for emails. Run it from multiple schedulers for parallelism: the base class uses optimistic row locking, so two pollers won't process the same job twice — the atomic claim means the poller that loses the race simply doesn't get that row and moves on. The extra counts cover the multi-server cases: `fenced` is attempts whose result was discarded because the job had been reaped and reclaimed, `skipped` is [exclusive jobs](/v4-2-0/digging-deeper/running-jobs-on-multiple-servers/#exclusive-jobs-and-concurrency-keys) whose lease was held (put back to `pending`), and `leasesLost` is runs that finished but had lost their lease. See [the guarantees](/v4-2-0/digging-deeper/running-jobs-on-multiple-servers/#the-guarantees).

Check queue health without tailing logs — `queueStats()` returns per-status counts:

Expand Down Expand Up @@ -447,6 +451,11 @@ Run the purge from a cron or scheduled task. A weekly purge of completed rows ol
## Related guides

<CardGrid>
<LinkCard
title="Running Jobs on Multiple Servers"
description="Process one queue from several servers — fencing, heartbeats, idempotency, per-host caps, and draining for deploys."
href="/v4-2-0/digging-deeper/running-jobs-on-multiple-servers/"
/>
<LinkCard
title="Models and the ORM"
description="Jobs persist as rows. Read this first if `model().findByKey()` and callbacks aren't second nature yet."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ The Digging Deeper section covers Wheels' advanced features — the ones you rea
href="/v4-2-0/digging-deeper/background-jobs/"
description="Queue work for asynchronous processing — retries, priorities, and the worker process."
/>
<LinkCard
title="Running Jobs on Multiple Servers"
href="/v4-2-0/digging-deeper/running-jobs-on-multiple-servers/"
description="Process one queue from several servers — fencing, heartbeats, idempotency, per-host caps, and draining for deploys."
/>
<LinkCard
title="Caching"
href="/v4-2-0/digging-deeper/caching/"
Expand Down
Loading
Loading