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.
Heartbeats and stall detection
Section titled “Heartbeats and stall detection”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)});const worker = new Worker('queue', processor, { heartbeatIntervalS: 5, // Ping every 5 seconds (0 = disabled)});worker = Worker("queue", process, heartbeat_interval_s=5.0) # 0 disables$worker = new Worker('queue', $processor, [ 'heartbeatIntervalS' => 5.0, // Fires between jobs (sequential worker)]);worker := bunqueue.NewWorker("queue", processor, bunqueue.WorkerOptions{ HeartbeatIntervalS: 5, // Heartbeats are disabled by default in Go})let worker = Worker::new("queue", processor, WorkerOptions { heartbeat_interval: Some(Duration::from_secs(5)), // None disables ..Default::default()});worker = Bunqueue.Worker.new("queue", handler, heartbeat_interval: 5_000) # msKeep the heartbeat interval shorter than the queue’s stallInterval to avoid false positives. See Stall Detection.
Lock-based ownership
Section titled “Lock-based ownership”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});const worker = new Worker('queue', async (job) => { await job.extendLock(60_000); // optional explicit lease extension return processor(job);}, { lockTtlMs: 30_000 }); // lock ownership cannot be disableddef process(job): job.extend_lock(60_000) # optional explicit lease extension return handle(job)
worker = Worker("queue", process, lock_ttl_ms=30_000)$worker = new Worker('queue', function (Bunqueue\Job $job) { // PHP is sequential, so extend manually before work longer than the TTL. $job->extendLock(60000); return processJob($job);}, ['lockTtlMs' => 30000]);worker := bunqueue.NewWorker("queue", func(job *bunqueue.Job) (any, error) { if err := job.ExtendLock(60_000); err != nil { return nil, err } return processor(job)}, bunqueue.WorkerOptions{LockTtlMs: 30_000})let worker = Worker::new("queue", |job| { job.extend_lock(60_000) .map_err(|error| ProcessError::retryable(error.to_string()))?; processor(job)}, WorkerOptions { lock_ttl_ms: 30_000, ..Default::default()});# Elixir always uses lock ownership and renews it automatically.worker = Bunqueue.Worker.new("queue", handler, lock_ttl: 30_000)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).
Where to go next
Section titled “Where to go next”| Worker | Create a worker and process your first job |
| Worker Concurrency and Batch Pulling | Run jobs in parallel and pull them in batches |
| The Job Object Inside a Worker Processor | Everything the processor receives and can do |
| Worker Events | completed, failed, stalled and the rest |
| Worker Error Handling, Retries and Backoff | Retries, backoff, timeouts and giving up |
| Worker Lifecycle | Pause, resume and shut down without losing work |
| SandboxedWorker | Experimental isolation for CPU-heavy handlers |
| WorkerOptions Reference | Every WorkerOptions field, with defaults |