# Worker Events: completed, failed, stalled

Every event a bunqueue Worker emits, with payload signatures: completed, failed, progress, stalled, active, drained, log and closed, plus how to unsubscribe.

Canonical: https://bunqueue.dev/guide/worker/events/

---

import { Tabs, TabItem } from '@astrojs/starlight/components';

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">guide · worker</span>
  <h1 class="bq-hero-h1 bq-bench-h1">What the worker <em>tells you.</em></h1>
  <p class="bq-hero-sub">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.</p>
</div>

## React to events

<Tabs syncKey="lang">
<TabItem label="Bun">

```typescript
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 attach
```

</TabItem>
<TabItem label="Node.js / Deno">

```typescript
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 attach
```

</TabItem>
<TabItem label="Python">

```python
worker.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))
```

</TabItem>
<TabItem label="PHP">

```php
$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"));
```

</TabItem>
<TabItem label="Go">

```go
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]) })
```

</TabItem>
<TabItem label="Rust">

Rust exposes transport and worker-retry telemetry rather than per-job event
listeners:

```rust
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()
});
```

</TabItem>
<TabItem label="Elixir">

Elixir exposes connection telemetry; record per-job outcomes in the handler's
normal return path:

```elixir
telemetry = fn event -> Logger.info("bunqueue", bunqueue: event) end

worker =
  Bunqueue.Worker.new("queue", handler,
    connection: [event_handler: telemetry]
  )
```

</TabItem>
</Tabs>

*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](/guide/sdks/#worker-events).*

All shared TypeScript client events are fully typed. The complete list (other SDK event sets are listed in the [SDK guide](/guide/sdks/#worker-events)):

| 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 both TypeScript packages, `stalled` is queue-scoped in 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

| Guide | What it covers |
|---|---|
| [Worker](/guide/worker/) | Create a worker and process your first job |
| [Worker Concurrency and Batch Pulling](/guide/worker/concurrency/) | Run jobs in parallel and pull them in batches |
| [The Job Object Inside a Worker Processor](/guide/worker/job-object/) | Everything the processor receives and can do |
| [Worker Error Handling, Retries and Backoff](/guide/worker/errors/) | Retries, backoff, timeouts and giving up |
| [Worker Lifecycle](/guide/worker/lifecycle/) | Pause, resume and shut down without losing work |
| [Heartbeats, Stall Detection and Lock Ownership](/guide/worker/stalls/) | Heartbeats, stall recovery and lock ownership |
| [SandboxedWorker](/guide/worker/sandboxed/) | Experimental isolation for CPU-heavy handlers |
| [WorkerOptions Reference](/guide/worker/options/) | Every WorkerOptions field, with defaults |