# Dead Letter Queue: What Happens to Failed Jobs

bunqueue keeps terminally failed jobs in a Dead Letter Queue with failure metadata. Inspect them, retry manually, and configure retention.

Canonical: https://bunqueue.dev/guide/dlq/

---

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

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">guide · dead letter queue</span>
  <h1 class="bq-hero-h1 bq-bench-h1">Failed jobs, kept in the <em>DLQ.</em></h1>
  <p class="bq-hero-sub">When a job runs out of retries, bunqueue can retain it in the Dead Letter Queue with the terminal error and attempt metadata, so you can inspect it and retry it deliberately.</p>
</div>

The Dead Letter Queue (DLQ) is a holding area for jobs that failed permanently,
for example after exhausting all retry attempts. Unless `removeOnFail` is set,
each entry keeps the original job, its terminal error, and a complete ordered
array of `AttemptRecord` values. The history includes retryable failures before
the terminal attempt and remains attached across automatic DLQ redeliveries.

Entries are not permanent by definition: `maxAge`, `maxEntries`, an explicit
purge, or `Queue.obliterate()` can remove them. The defaults retain entries for
seven days and cap each queue at 10,000 entries. Capacity eviction removes the
oldest entry and its terminal ownership/result/log data from the selected
backend: in-memory state, SQLite, or PostgreSQL. Persistent-backend deletion is
committed atomically with the eviction.

## Quick Start

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

```typescript
import { Queue } from 'bunqueue/client';

const queue = new Queue('emails', { embedded: true });

// See what failed and why
const entries = queue.getDlq();
for (const entry of entries) {
  console.log(entry.job.id, entry.reason, entry.error);
}

// Put everything back in the queue for another try
queue.retryDlq();
```

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

```typescript
import { Queue } from 'bunqueue-client';

const queue = new Queue('emails');

// List the dead jobs (over TCP they arrive as plain jobs,
// without DLQ metadata like the failure reason)
const jobs = await queue.getDlq();
for (const job of jobs) {
  console.log(job.id);
}

// Put everything back in the queue for another try
await queue.retryDlq();
```

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

```python
from bunqueue import Queue

queue = Queue("emails")

# List the dead jobs (over TCP they arrive as plain jobs,
# without DLQ metadata like the failure reason)
for job in queue.get_dlq():
    print(job["id"])

# Put everything back in the queue for another try
queue.retry_dlq()
```

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

```php
use Bunqueue\Queue;

$queue = new Queue('emails');

// List the dead jobs (over TCP they arrive as plain jobs,
// without DLQ metadata like the failure reason)
foreach ($queue->getDlq() as $job) {
    echo $job['id'], PHP_EOL;
}

// Put everything back in the queue for another try
$queue->retryDlq();
```

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

```go
queue := bunqueue.NewQueue("emails", bunqueue.Options{})
defer queue.Close()

// List the dead jobs (over TCP they arrive as plain jobs,
// without DLQ metadata like the failure reason)
jobs, err := queue.GetDlq(0)
for _, job := range jobs {
    fmt.Println(job["id"])
}

// Put everything back in the queue for another try
_, err = queue.RetryDlq("", 0)
```

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

```rust
use bunqueue_client::{ConnectionOptions, Queue};

let queue = Queue::new("emails", ConnectionOptions::default());

// List the dead jobs (over TCP they arrive as plain jobs,
// without DLQ metadata like the failure reason)
let jobs = queue.get_dlq(None)?;
println!("{} dead jobs", jobs.len());
for job in &jobs {
    println!("{job:?}");
}

// Put everything back in the queue for another try
queue.retry_dlq(None, None)?;
```

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

```elixir
queue = Bunqueue.queue("emails")

# List the dead jobs (over TCP they arrive as plain jobs,
# without DLQ metadata like the failure reason)
{:ok, jobs} = Bunqueue.Queue.dlq(queue)
for job <- jobs, do: IO.puts(job["id"])

# Put everything back in the queue for another try
{:ok, _count} = Bunqueue.Queue.retry_dlq(queue)
```

</TabItem>
</Tabs>

From the CLI, against a running server:

```bash
bunqueue dlq list emails
bunqueue dlq retry emails
bunqueue dlq purge emails
```

:::note[Embedded vs TCP]
The synchronous query API on this page (`getDlq()` and `getDlqStats()`) reads
in-process state and therefore remains embedded-only. Synchronous TCP mutations,
including `retryDlqByFilter()`, are fire-and-forget and return `0`; the filtered
retry still reaches the broker. The `Async` variants work in both modes:
`getDlqAsync(filter?)` returns full metadata and operational Job objects,
`getDlqStatsAsync()` returns authoritative statistics,
`retryDlqByFilterAsync(filter)` applies server-side filtering and returns the
applied count, and the existing retry/purge/config async forms wait for the real
result. `getDlqJobsAsync(count?)` remains the compact jobs-only view.
:::

## Where to go next

| | |
|---|---|
| [DLQ Operations from the Queue Object](/guide/queue/dlq/) | The same operations from an existing Queue instance |
| [DLQ Operations](/guide/dlq/operations/) | Filter, retry selectively, check health, purge |
| [Automatic DLQ Retry with Backoff](/guide/dlq/auto-retry/) | Let bunqueue re-queue dead entries on a backoff |
| [DLQ Configuration](/guide/dlq/configuration/) | autoRetry, maxAge, maxEntries and the defaults |
| [DLQ Reference](/guide/dlq/reference/) | Failure reasons, entry shape, every DLQ method |