Skip to content
Get started
Get started
Heartbeats, Stall Detection and Lock Ownership
guide · worker

Proving the worker is still alive.

A crashed worker cannot tell anyone. Heartbeats let the queue notice, and lease tokens ensure only the current owner can settle a recovered job.

While a job is processing, the worker automatically pings the queue (“I’m still working on this”). That ping is the heartbeat. If a job stops receiving heartbeats, for example because the worker crashed, the queue marks it stalled and recovers it, so no job is silently lost.

const worker = new Worker('queue', processor, {
embedded: true,
heartbeatInterval: 5000, // Ping every 5 seconds (ms)
});

Keep the heartbeat interval shorter than the queue’s stallInterval to avoid false positives. See Stall Detection.

With useLocks: true (the default), each pulled job gets a lock, a temporary claim that says “this worker owns this job”. The lock is renewed by heartbeats (lockDuration sets its TTL) and must be presented when completing or failing the job. Delivery is exclusive while the lease is valid. After expiry, the broker may redeliver while the stale handler is still running, but its old token can no longer acknowledge the job. A redelivery gets a fresh token and a new local processing generation even when the same Worker instance receives it. Only that current generation is heartbeated and allowed to publish an automatic outcome; completion or cleanup from the stale handler cannot remove the new lease. This is at-least-once processing, so handlers must still be idempotent.

Locks matter most in server mode with multiple workers. In embedded mode with a single process you can trade the safety for a bit of throughput:

const worker = new Worker('queue', processor, {
embedded: true,
useLocks: false, // Rely on stall detection only
});

The external SDKs always use lock-based ownership: every pulled job carries a lock token whose TTL is set by lockTtlMs / lock_ttl_ms / LockTtlMs / lock_ttl, renewed by heartbeats. A single long-running handler can extend its own lease with job.extendLock(ms) (TypeScript, PHP), job.extend_lock(ms) (Python, Rust), or job.ExtendLock(ms) (Go).

In the Bun client, locks can also be extended explicitly: worker.extendJobLocks(jobIds, tokens, duration).

WorkerCreate a worker and process your first job
Worker Concurrency and Batch PullingRun jobs in parallel and pull them in batches
The Job Object Inside a Worker ProcessorEverything the processor receives and can do
Worker Eventscompleted, failed, stalled and the rest
Worker Error Handling, Retries and BackoffRetries, backoff, timeouts and giving up
Worker LifecyclePause, resume and shut down without losing work
SandboxedWorkerExperimental isolation for CPU-heavy handlers
WorkerOptions ReferenceEvery WorkerOptions field, with defaults