Worker processes
Run workers as dedicated processes with one configuration file, graceful drain on SIGTERM, and probe endpoints for your orchestrator.
Your web tier and your job workers have different failure modes, different scaling curves, and different memory budgets. A dedicated worker process keeps them apart: web replicas serve traffic, worker replicas process jobs, and the two meet only in PostgreSQL. Workhorse packages the whole process lifecycle — startup, signals, drain, probes — so a worker deployment is one configuration file and one command.
If the web tier runs in a function or edge isolate, use the serverless and edge matrix to check its enqueue path. The worker still runs here, in continuous compute with its own process lifecycle.
Define the process
defineWorkerProcess type-checks a configuration and returns it unchanged. The definition owns
its adapter — and through it, its database pool — plus one entry per worker.
import { createWorkhorseAdapter, defineWorkerProcess, Pool } from "@stablemates/workhorse";
import { generateReport, sendEmail } from "./jobs.js";
export default defineWorkerProcess({
adapter() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
return createWorkhorseAdapter({
database: pool,
adaptTransaction: (transaction: typeof pool) => transaction,
close: () => pool.end(),
});
},
workers: [
{
options: { concurrency: 8 },
configure(worker) {
worker.handle("email.send", sendEmail);
worker.handle("report.generate", generateReport);
},
},
],
shutdownTimeoutMs: 25_000,
probes: { hostname: "0.0.0.0", port: 9090 },
});Compile it, then run the default export with the packaged CLI:
workhorse worker --config ./dist/workhorse.worker.jsThe CLI imports JavaScript the current Node.js process understands — it bundles no TypeScript
loader, so production runs compiled output. --shutdown-timeout-ms overrides the configured
drain deadline per deployment.
If you already own process supervision, skip the CLI: startWorkerProcess(definition) starts
workers without installing global signal handlers, and runWorkerProcess(definition, options)
adds the standalone Node.js lifecycle — signal handling, exit codes, forced exit.
Drain, don't drop
On the first SIGTERM or SIGINT, the process marks readiness false and calls Worker.stop on
every worker. Workers stop asking PostgreSQL for jobs; active handlers keep their leases and
finish. If a claim was already in flight when the signal arrived, it can still commit — the
process now owns that lease, so it drains that job too.
The drain is bounded by shutdownTimeoutMs (25 seconds by default). If the deadline expires, the
process exits and leaves the remaining leases in PostgreSQL, where ordinary fenced recovery hands
them to another worker. Nothing is lost either way — a drained job finished here, an abandoned
one finishes elsewhere.
Two boundaries worth knowing:
- Process shutdown does not abort
HandlerContext.signaland creates no cancellation evidence. When the job itself must stop, callqueue.cancel. - Keep the drain deadline shorter than your platform's termination window. A second signal requests immediate exit, so handlers must stay safe for recovery after a forced stop — which checkpointed, idempotent handlers already are.
Probes for your orchestrator
The optional probe listener serves exactly two endpoints: liveness (/livez) while the process
exists, readiness (/readyz) while workers can still ask for jobs. Readiness drops the moment
draining begins, so a rolling deployment routes nothing to a process on its way out.
The probe server is not application ingress. It exposes no job data, no queue data, no metrics, and no mutations — safe to bind wherever your orchestrator needs it.
If startup fails partway, the process closes every resource it already created. If any worker loop exits unexpectedly, the process stops its siblings and exits, so your supervisor restarts the whole unit rather than running it half-alive.
One process, one pool
Give each process its own pool and let the adapter's close shut it down after the last worker
drains. Size total capacity across replicas: heartbeats, handler queries, and maintenance share
those connections. Each worker also registers itself in PostgreSQL on its registryIntervalMs
cadence, which is what lets a dashboard on another machine see the fleet — see
Operations.
Next
- Workers — handlers, concurrency, and local lifecycle
- Maintenance and retention — the background work these processes drive
- Cancellation — stop one job instead of one process
Exact signal handling, shutdown deadlines, probes, and recovery behavior: architecture reference.