# 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](/docs/serverless)
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.

```ts title="src/workhorse.worker.ts"
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:

```bash
workhorse worker --config ./dist/workhorse.worker.js
```

The 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.signal` and creates no cancellation evidence.
  When the job itself must stop, call `queue.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](/docs/operations).

## Next

- [Workers](/docs/workers) — handlers, concurrency, and local lifecycle
- [Maintenance and retention](/docs/maintenance) — the background work these processes drive
- [Cancellation](/docs/cancellation) — stop one job instead of one process

---

Exact signal handling, shutdown deadlines, probes, and recovery behavior:
[architecture reference](https://github.com/stablemates/workhorse/blob/main/docs/architecture.md#worker-process-lifecycle).
