The process that actually runs jobs: claim due rows, execute them, record the outcome, repeat.
Its own Fly process group, declared in fly.toml beside the existing app group — not inside the web process. Job load and request serving then cannot contend for the same event loop, and the two scale independently. The [processes] entry is this card's deliverable; scaling it to a running count is an operator action and belongs to its own manual sibling.
Claiming is a read-derived write: the loop reads which rows are due, then writes a claim based on what it read. Two workers doing that with a plain read-then-write both see the same row and both run the job. So the claim is:
SELECT … FROM job_queue
WHERE state = 'pending' AND run_at <= now()
ORDER BY run_at
FOR UPDATE SKIP LOCKED
LIMIT n
inside one transaction, with the state write in the same transaction. SKIP LOCKED is what makes a second worker take the NEXT row rather than block on the first.
A serial test cannot see this defect. Two sequential claims never collide; the race needs genuine concurrency against a warm pool, and that is what the acceptance criteria demand.
LISTEN/NOTIFY (or the chosen library's equivalent) so a freshly emitted event does not wait out a poll interval.running forever. The lease duration and its renewal are this card's decisions and belong in a comment.SIGTERM, stop claiming, let in-flight runs finish or checkpoint, release claims. A deploy is a routine event and must not orphan work.job_queue row twice — asserted by a test that drives genuine concurrency and accepts every legitimate interleaving, not a serial one.SIGTERM drains rather than aborting: no run is left running with no live claimant after a shutdown.[processes] worker group is declared in fly.toml; the card does NOT scale it.fly.toml — the [processes] block, and min_machines_running as the precedent for how process groups are declared hereprisma/schema.prisma — job_queue from the schema cardmotir-core/CLAUDE.md § concurrency — lock-before-a-contended-update and the warm-pool racelib/services/codeGraphIndexAdmissionService.ts — an existing FOR UPDATE claim in this codebase, for shape