# DLQ Operations: Filter, Retry, Remove, Purge

Everyday work against the bunqueue Dead Letter Queue: filter entries by reason or age, retry a subset, permanently remove one entry, read health counters and purge what you do not need.

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

---

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">Read it, retry it, <em>clear it.</em></h1>
  <p class="bq-hero-sub">The four things you actually do with a Dead Letter Queue: narrow it down to the entries you care about, put a subset back, check whether the pile is growing, and empty it when the cause is fixed.</p>
</div>

## Common Tasks

### Filter entries

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

```typescript
await queue.getDlqAsync({ reason: 'max_attempts_exceeded' });         // by emitted reason
await queue.getDlqAsync({ olderThan: Date.now() - 86400000 });        // older than 24h
await queue.getDlqAsync({ newerThan: Date.now() - 3600000 });         // last hour
await queue.getDlqAsync({ retriable: true });                         // auto-retry is due now
await queue.getDlqAsync({ limit: 10, offset: 20 });                   // pagination
```

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

The SDK's `getDlq()` returns jobs only. Use the HTTP endpoint for full entry
metadata, then filter the returned page in the client:

```typescript
const response = await fetch('http://localhost:6790/queues/emails/dlq?limit=100&offset=0');
const { entries, total } = await response.json();
const stalled = entries.filter((entry) => entry.reason === 'stalled');
```

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

The SDK's `get_dlq()` returns jobs only. The standard library can read full
entries from HTTP:

```python
import json
from urllib.request import urlopen

with urlopen("http://localhost:6790/queues/emails/dlq?limit=100&offset=0") as response:
    payload = json.load(response)
stalled = [entry for entry in payload["entries"] if entry["reason"] == "stalled"]
```

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

The SDK's `getDlq()` returns jobs only. Read full entries through HTTP:

```php
$json = file_get_contents('http://localhost:6790/queues/emails/dlq?limit=100&offset=0');
$payload = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
$stalled = array_filter($payload['entries'], fn ($entry) => $entry['reason'] === 'stalled');
```

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

The SDK's `GetDlq()` returns jobs only. Read full entries through HTTP with the
standard library:

```go
response, err := http.Get("http://localhost:6790/queues/emails/dlq?limit=100&offset=0")
if err != nil { return err }
defer response.Body.Close()

var payload struct { Entries []map[string]any `json:"entries"` }
err = json.NewDecoder(response.Body).Decode(&payload)
```

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

The Rust SDK's `get_dlq()` returns jobs only. Use the broker HTTP endpoint (and
your application's HTTP/JSON crate) for full entries:

```bash
curl 'http://localhost:6790/queues/emails/dlq?limit=100&offset=0'
```

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

The Elixir SDK's `dlq/2` returns jobs only. Use the broker HTTP endpoint for
full entries:

```bash
curl 'http://localhost:6790/queues/emails/dlq?limit=100&offset=0'
```

</TabItem>
</Tabs>

The Bun async API applies `reason`, time, due-retry, expiry, limit, and offset
filters server-side in both embedded and TCP modes. The HTTP endpoint currently
supports only `limit` and `offset`; external clients must filter that page
locally. `retriable: true` means `nextRetryAt` is already due, not merely that
the entry has retry budget remaining.

### Retry selectively

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

```typescript
queue.retryDlq();                                          // retry everything
queue.retryDlq('job-123');                                 // retry one job
queue.retryDlqByFilter({ reason: 'stalled' });             // TCP is fire-and-forget
const filtered = await queue.retryDlqByFilterAsync({ reason: 'stalled' }); // authoritative count
const n = await queue.retryDlqAsync();                     // retry and get the count (TCP too)
```

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

```typescript
await queue.retryDlq();                       // retry everything
await queue.retryDlq('job-123');              // retry one job
await queue.retryJobs({ count: 100 });        // retry only the first 100 entries
```

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

```python
queue.retry_dlq()                    # retry everything
queue.retry_dlq("job-123")           # retry one job
queue.retry_dlq(count=100)           # retry only the first 100 entries
```

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

```php
$queue->retryDlq();                  // retry everything
$queue->retryDlq('job-123');         // retry one job
$queue->retryDlq(null, 100);         // retry only the first 100 entries
```

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

```go
n, err := queue.RetryDlq("", 0)         // retry everything
n, err = queue.RetryDlq("job-123", 0)   // retry one job
n, err = queue.RetryDlq("", 100)        // retry only the first 100 entries
```

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

```rust
queue.retry_dlq(None, None)?;              // retry everything
queue.retry_dlq(Some("job-123"), None)?;   // retry one job
queue.retry_dlq(None, Some(100))?;         // retry only the first 100 entries
```

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

```elixir
{:ok, n} = Bunqueue.Queue.retry_dlq(queue)               # retry everything
{:ok, _} = Bunqueue.Queue.retry_dlq(queue, "job-123")    # retry one job
{:ok, _} = Bunqueue.Queue.retry_dlq(queue, nil, 100)     # retry only the first 100
```

</TabItem>
</Tabs>

### Check DLQ health

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

```typescript
const stats = await queue.getDlqStatsAsync();
console.log(stats.total);         // total entries
console.log(stats.byReason);      // { max_attempts_exceeded: 5, stalled: 2, ... }
console.log(stats.pendingRetry);  // entries whose auto-retry time is due
```

A simple alert loop:

```typescript
setInterval(async () => {
  const stats = await queue.getDlqStatsAsync();
  if (stats.total > 100) alertOps('High DLQ count', stats);
}, 30000);
```

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

```typescript
const { stats } = await (await fetch(
  'http://localhost:6790/queues/emails/dlq/stats'
)).json();
```

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

```python
with urlopen("http://localhost:6790/queues/emails/dlq/stats") as response:
    stats = json.load(response)["stats"]
```

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

```php
$payload = json_decode(
    file_get_contents('http://localhost:6790/queues/emails/dlq/stats'),
    true,
    flags: JSON_THROW_ON_ERROR,
);
$stats = $payload['stats'];
```

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

```go
response, err := http.Get("http://localhost:6790/queues/emails/dlq/stats")
// Decode response.Body and read the top-level "stats" object.
```

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

```bash
curl 'http://localhost:6790/queues/emails/dlq/stats'
```

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

```bash
curl 'http://localhost:6790/queues/emails/dlq/stats'
```

</TabItem>
</Tabs>

Use synchronous `getDlqStats()` for a Bun embedded snapshot and
`getDlqStatsAsync()` for an authoritative Bun result in either runtime. The
other SDKs do not expose a stats helper yet; the HTTP result is authoritative.

### Permanently remove one failed job

```typescript
const removed = await queue.removeDlqJob('job-123');
// true: the selected DLQ entry was deleted
// false: it was already absent
```

`removeDlqJobAsync(id)` is an explicit alias with the same
`Promise<boolean>` contract. The operation does not retry the job. It removes
the durable entry and terminal auxiliary state before resolving, including any
recovered duplicate rows for the same queue and job ID. Broker and persistence
errors reject the Promise; only a successful miss resolves `false`.

### Purge

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

```typescript
const purged = queue.purgeDlq();             // permanently deletes all entries
const n = await queue.purgeDlqAsync();       // same, but waits and returns the count (TCP too)
```

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

```typescript
const purged = await queue.purgeDlq();       // permanently deletes all entries, returns the count
```

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

```python
purged = queue.purge_dlq()       # permanently deletes all entries, returns the count
```

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

```php
$purged = $queue->purgeDlq();    // permanently deletes all entries, returns the count
```

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

```go
purged, err := queue.PurgeDlq()  // permanently deletes all entries, returns the count
```

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

```rust
let purged = queue.purge_dlq()?; // permanently deletes all entries, returns the count
```

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

```elixir
{:ok, purged} = Bunqueue.Queue.purge_dlq(queue)  # deletes all entries, returns the count
```

</TabItem>
</Tabs>

## Where to go next

| | |
|---|---|
| [DLQ Operations from the Queue Object](/guide/queue/dlq/) | The same operations from an existing Queue instance |
| [Dead Letter Queue](/guide/dlq/) | What the DLQ is, and a first look at what failed |
| [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 |