What the worker tells you.
Metrics, alerting and audit trails all hang off the same handful of events. Here is each one, when it fires and exactly what it hands your listener.
React to events
Section titled “React to events”worker.on('completed', (job, result) => { console.log(`Completed: ${job.id}`, result);});
worker.on('failed', (job, error) => { console.error(`Failed: ${job.id}`, error.message);});
worker.on('progress', (job, progress) => { console.log(`Progress: ${job.id} - ${progress}%`);});
worker.on('error', (error) => console.error(error)); // always attachworker.on('completed', (job, result) => { console.log(`Completed: ${job.id}`, result);});
worker.on('failed', (job, error) => { console.error(`Failed: ${job.id}`, error.message);});
worker.on('error', (err) => console.error(err)); // always attachworker.on("completed", lambda job, result: print("completed", job.id))worker.on("failed", lambda job, err: print("failed", job.id, err))worker.on("progress", lambda job, progress: print("progress", job.id, progress))$worker->on('completed', fn ($job, $result) => print("completed {$job->id()}\n"));$worker->on('failed', fn ($job, $err) => print("failed {$job->id()}\n"));$worker->on('error', fn ($err) => print($err->getMessage() . "\n"));worker.On("completed", func(args ...any) { job := args[0].(*bunqueue.Job) log.Printf("completed %s", job.ID())})worker.On("error", func(args ...any) { log.Println(args[0]) })Rust exposes transport and worker-retry telemetry rather than per-job event listeners:
use std::sync::Arc;use bunqueue_client::{ ConnectionOptions, TelemetryCallback, TelemetryEvent, Worker, WorkerOptions,};
let telemetry: TelemetryCallback = Arc::new(|event| match event { TelemetryEvent::WorkerRetry { queue, message, .. } => { eprintln!("{queue}: retrying after {message}"); } TelemetryEvent::Error { operation, message } => { eprintln!("{operation}: {message}"); } _ => {}});
let worker = Worker::new("queue", processor, WorkerOptions { connection: ConnectionOptions { telemetry: Some(telemetry), ..Default::default() }, ..Default::default()});Elixir exposes connection telemetry; record per-job outcomes in the handler’s normal return path:
telemetry = fn event -> Logger.info("bunqueue", bunqueue: event) end
worker = Bunqueue.Worker.new("queue", handler, connection: [event_handler: telemetry] )Rust and Elixir telemetry covers connection, command, retry, timeout and error
lifecycle, but it does not synthesize completed/failed listener events.
Record per-job outcomes inside the processor. See the
SDK guide.
All Bun client events are fully typed. The complete list (external SDK event sets are listed in the SDK guide):
| Event | Callback Parameters | Description |
|---|---|---|
ready | () | Worker started polling |
active | (job: Job<T>) | Job started processing |
completed | (job: Job<T>, result: R) | Job completed successfully |
failed | (job: Job<T>, error: Error) | Job processing failed |
progress | (job: Job<T> | null, progress: number) | Job progress updated |
stalled | (jobId: string, reason: string) | Job stalled (no heartbeat) |
drained | () | Queue has no more waiting jobs |
error | (error: Error) | Worker-level error |
cancelled | ({ jobId: string, reason: string }) | Job was cancelled |
log | (job: Job<T>, message: string) | Log written via job.log() |
closed | () | Worker shut down |
For Bun Workers, completed and failed are broker-authoritative. The Worker
emits the local terminal event only after the broker accepts that exact lease
generation. If the broker’s processing timeout already failed or requeued the
job while a processor was still returning, its late ACK/FAIL is an idempotent
no-op and no contradictory local terminal event or Worker error is emitted.
This rule is identical for Embedded, TCP, manual job.moveToFailed(), and
SandboxedWorker execution.
The same ownership rule covers nonterminal processor mutations. After the
broker accepts retry(), changeDelay(), moveToWait(), moveToDelayed(),
moveToWaitingChildren(), or discard(), the Worker emits no local
completed/failed event for the retired generation and sends no automatic
ACK/FAIL. A real asynchronous discard() rejection emits exactly one Worker
error; an authoritative already-absent result is silent.
For the Bun client, stalled is queue-scoped in both embedded and TCP mode.
TCP workers receive it through a dedicated authenticated event connection that
automatically re-subscribes after reconnect. skipStalledCheck: true disables
only that listener, never broker-side stall recovery. The event is also emitted
when an expired lock exhausts the queue’s retry or stall budget; in that case
the broker records stalled before the terminal failed queue event and DLQ
transition.
Remove a listener with worker.off('completed', handler).
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 Error Handling, Retries and Backoff | Retries, backoff, timeouts and giving up |
| Worker Lifecycle | Pause, resume and shut down without losing work |
| Heartbeats, Stall Detection and Lock Ownership | Heartbeats, stall recovery and lock ownership |
| SandboxedWorker | Experimental isolation for CPU-heavy handlers |
| WorkerOptions Reference | Every WorkerOptions field, with defaults |