Workers
Register handlers, size concurrency, tune the lease, and drain active work safely.
Jobs sit in PostgreSQL until a process runs them. That process is a Worker: a loop that
claims ready jobs, runs your handlers, and records each result. This page covers everything
between new Worker(queue) and a clean shutdown.
Register handlers and run
Worker.handle binds a job type to an async function. Registration chains, so one worker can
serve many types. Each handler receives the payload and a HandlerContext, and returns a JSON
result that becomes the job's durable outcome.
const worker = new Worker(queue, { queues: ["email", "billing"], concurrency: 5 });
worker.handle("email.send", async (payload: { to: string }, ctx) => {
await mailer.send(payload.to, { signal: ctx.signal });
return { deliveredTo: payload.to };
});
await worker.run();run loops until you stop it; pass an AbortSignal as worker.run(signal) to tie its
lifetime to your process. runOnce performs a single claim-and-run pass and returns whether
it found work — useful in tests and scripts.
Python exposes this lifecycle through synchronous Worker and asynchronous AsyncWorker.
Both rotate across configured queues, fill a bounded set of slots, and drain active handlers before
run returns. AsyncWorker uses dedicated native Psycopg or asyncpg query and notification
connections, and its handler context methods are awaitable. Both surfaces share the same claim,
heartbeat, batch, settlement, telemetry, and drain core. Workers in all three languages also
participate in the worker registry.
Python's handle_batch follows the same grouping contract described in
Batch handlers.
Waiting without constant polling
An idle worker listens for committed wake hints and receives only notifications for its configured queues. Promotion and recovery notify each affected queue separately, so activity on one queue does not wake workers assigned to another. Notifications wake dispatch without changing the cadence of maintenance or worker registration, while bounded polling still covers a lost listener or message. Workers briefly stagger notification-triggered claims. Workers without an active listener back off after consecutive empty checks and reset that delay when a claim succeeds.
The HandlerContext carries the claimed job, its AbortSignal, checkpoints, durable waits,
and progress. Every context method writes under the current fence, so handler code cannot
accidentally write as a stale owner after the job moves on.
The options that matter
const worker = new Worker(queue, {
queues: ["email", "billing"], concurrency: 5, leaseMs: 30_000,
workerId: "email-worker-1",
});concurrencycaps simultaneous handlers in this oneWorker. Replicas multiply that capacity. When the fleet must share one budget, use concurrency policies; for a shared start rate, use rate limits.leaseMsis how long a claim owns a job before the database may recover it. One background timer batches every active lease owned by the worker — everyheartbeatMs, defaulting to a third ofleaseMs. A shorter lease means faster recovery after a crash; a longer one tolerates worse network pauses.workerIdidentifies the lease owner. Leave it generated unless a process manager guarantees uniqueness: two live workers sharing one identity collide over ownership. The default embeds host and pid, so fleet views stay readable.retryDelayMsoverrides the persisted retry policy — a number, or(attempt, job) => number | undefined, whereundefineddefers to the database.scheduleNamespacesandscheduleCatchupLimitopt this worker into offering recurring schedules.
Python uses schedule_namespaces and schedule_catchup_limit for the same boundary. Go uses
WorkerOptions.ScheduleNamespaces and WorkerOptions.ScheduleCatchupLimit.
queueslets one worker rotate across several queues under the same identity and concurrency budget. Usequeuefor one queue; omit both options to use the client default.
Group queues that share handlers and operational policy into one multi-slot worker. Add a second worker when work needs a different lease, retry override, schedule namespace, or handler registry.
Process jobs in batches
Worker.handleBatch groups claimed jobs of one type when one provider call can process several
payloads efficiently. Every member still occupies one concurrency slot and keeps its own lease,
fence, cancellation signal, progress, retry budget, and final result.
The TypeScript handleBatch, Python handle_batch, and Go HandleBatch forms are shown together
in Batch handlers, including their positional success and failure results.
The callback must return one ordered outcome per member. Batch contexts omit durable waits, signals, human decisions, and child joins because one member cannot suspend independently inside a shared invocation. See Batch handlers for grouping and failure rules.
Run workers in a dedicated process
Queue depth and HTTP traffic rarely scale together, so production deployments put workers in
their own process. defineWorkerProcess describes the adapter and workers; the
workhorse worker CLI loads the compiled definition and adds signal handling and supervision.
export default defineWorkerProcess({
adapter: () => createWorkhorseAdapter(adapterOptions),
workers: [{
options: { queues: ["email", "billing"], concurrency: 5 },
configure(worker) { worker.handle("email.send", sendEmail); },
}],
});The process owns its database resources, worker loops, optional probes, and shutdown. Compile the definition before the CLI imports it — the CLI does not install a TypeScript loader.
The first termination signal stops claims and drains active jobs. A second signal or an expired deadline exits immediately, leaving PostgreSQL to recover any active leases.
Go applications create the same boundary with signal.NotifyContext, then pass that context to
Worker.Run. A handler panic becomes a recorded attempt failure, so it does not terminate the
worker process or prevent later jobs from running.
Pause, stop, and drain
Three verbs cover controlled slowdown, and each answers a different situation:
worker.pause()stops this worker from claiming while active handlers finish.worker.resume()lets it claim again;worker.isPaused()reads the effective state.worker.stop()drains: no new claims, active handlers run to completion, thenrunresolves. OnSIGTERM, the worker process does this for you.Admin.setWorkerPausedstores an operator pause inworker_registry. TypeScript, Python, and Go workers pick it up on their next registration refresh, so a dashboard can pause a process it does not host. A local resume cannot clear an operator pause, and an operator pause dies with the process incarnation it named.
If work must stop durably — surviving worker restarts — pause the queue with
Admin.pauseQueue, not the worker.
Read fleet state carefully
Worker.runtimeState() reads a TypeScript worker's local process state. The dashboard and
Admin.listWorkers read the durable registry that every runtime refreshes on an interval. Registry
slot counts are recent observations, not synchronous views of another event loop — treat them as a
moment-ago snapshot.
TypeScript configures the cadence with registryIntervalMs, Python uses registry_interval_ms,
and Go uses RegistryInterval. A registration error does not stop dispatch, and the worker keeps
its last remote-pause decision until a later refresh succeeds.
A worker that dies stops refreshing and goes stale in the registry. Automatic maintenance makes that row eligible for removal after one minute. Its jobs are not lost: their leases expire and recovery makes them claimable again.
The protocol names make this lifecycle greppable. claim_many_v1 applies claim_v1 to each
selected job and writes its job_runtime lease. heartbeat_many_v1 submits every active lease
and extends the accepted expires_at values. recover_expired_v1 makes abandoned work ready.
Workers claim from workhorse_jobs, TypeScript configures fallback polling with pollMs, and
processes commonly begin graceful drain after SIGINT.
Next
- Deployment and operations — supervise and drain the worker process
- Batch handlers — share one application call across jobs
- Cancellation — make handlers stop cooperatively
- Schedules — let workers offer recurring namespaces
Exact worker options, registry fields, heartbeat behavior, and process lifecycle: architecture reference.