---
title: "Dead-letter queues"
description: "Move messages that keep failing to a dead-letter queue, inspect them without consuming them, and redrive them back."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.ezghcloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dead-letter queues

A dead-letter queue (DLQ) holds messages that were received too many times without being
acknowledged, so one message that always fails doesn't keep being retried. It's an ordinary queue
that another queue's redrive policy names.

## Set a redrive policy

Create the dead-letter queue first, of the same type, then name it in the source queue's
`redrive`, at creation or with [UpdateQueue](/queues-api/UpdateQueue/):

```sh
curl -sS -X PATCH "$BASE/queues/$QUEUE_ID" \
  -H "Authorization: Bearer $EZGH_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{ \"redrive\": { \"deadLetterQueue\": \"$DLQ_ID\", \"maxReceiveCount\": 5 } }"
```

- **`deadLetterQueue`** is the DLQ's ID: a queue in the same project, of the same type
  (`400 queue_type_mismatch` otherwise), and not the queue itself.
- **`maxReceiveCount`** is 1 to 1,000.
- `"redrive": null` removes the policy.

Several queues can share one dead-letter queue. A queue that's another queue's DLQ can't be deleted
(`409 queue_in_use`) until that policy is removed.

With the CLI: `ezgh queues update orders --dlq orders-dlq --max-receives 5`, and `--no-dlq` to
remove the policy.

## When a message moves

A message is delivered at most `maxReceiveCount` times. When a receive, or a
[push subscription](/queues/push) delivery, finds a message that has already been received that
many times, it moves the message to the DLQ instead of delivering it. Every push delivery attempt
counts as a receive.

In the dead-letter queue the message:

- keeps its `messageId`, body and attributes, and gains two attributes: `ezgh.sourceQueue`, the
  source queue's ID, and `ezgh.deadLetteredAt`, when it moved (RFC 3339);
- is visible at once, with `receiveCount` back to 0;
- **keeps its original `sentAt`, and expires at `sentAt` plus the DLQ's retention.** Give the DLQ a
  longer retention than its source queues, or messages that took long to fail expire soon after
  they arrive, or at once.

On a FIFO queue, a dead-lettered message no longer holds up its group: the group continues with
its next message.

## Inspect messages

[PeekMessages](/queues-api/PeekMessages/) lists a queue's messages in the order they were
sent, without receiving them: nothing is claimed or changed, and no receipt handles are returned.

```sh
curl -sS "$BASE/queues/$DLQ_ID/messages?limit=20" -H "Authorization: Bearer $EZGH_API_KEY"
```

```json
{
  "messages": [
    {
      "messageId": "01999f3d-02b4-7c1d-8e2f-3a4b5c6d7e8f",
      "body": "{\"orderId\": \"o-123\"}",
      "attributes": {
        "source": "checkout",
        "ezgh.sourceQueue": "01999f3c-7a1e-7b2a-9c4d-5e6f7a8b9c0d",
        "ezgh.deadLetteredAt": "2026-10-13T17:05:12Z"
      },
      "sentAt": "2026-10-13T17:02:40Z",
      "receiveCount": 0,
      "state": "visible"
    }
  ],
  "nextCursor": null
}
```

- `limit` is 1 to 100 (20 by default); follow `nextCursor` for more. `state` is `visible`,
  `inFlight` or `delayed`; FIFO messages add `groupId` and `seq`.
- Peeking needs `queues.messages.peek`, which `QueuesReadOnlyAccess` and `ReadOnlyAccess` include,
  and is recorded in [Trails](/trails) as a data event. A queue can be peeked 10 times a second.
- It works on any queue, but suits dead-letter queues and debugging. Consumers receive instead.

## Redrive messages back

Once you've fixed what made them fail, [StartRedrive](/queues-api/StartRedrive/) on the
dead-letter queue moves its messages back:

```sh
curl -sS -X POST "$BASE/queues/$DLQ_ID/redrives" \
  -H "Authorization: Bearer $EZGH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "id": "01999f60-4d5e-7f6a-8b9c-0d1e2f3a4b5c",
  "queueId": "01999f3b-9e8d-7c6b-a5f4-e3d2c1b0a9f8",
  "destinationQueueId": null,
  "status": "running",
  "moved": 0,
  "skipped": 0,
  "startedAt": "2026-10-13T18:00:00Z",
  "finishedAt": null
}
```

- **Which messages:** those dead-lettered before the redrive started. Messages that arrive later
  stay for the next redrive.
- **Where:** each back to the queue it came from, or every one to `destinationQueueId`, a queue in
  the same project and of the same type. Naming a destination needs `queues.redrives.start` on it
  as well as on the DLQ.
- **Skipped,** and left in the DLQ: messages in flight in the DLQ, and messages whose source queue
  was deleted. `skipped` counts them.
- **Moved messages** get `receiveCount` 0 and lose the `ezgh.` attributes. They keep their
  original `sentAt`, and expire at `sentAt` plus their new queue's retention, so a message older
  than that retention expires as soon as it moves.
- Redrives run in the background at about 500 messages a second. Follow one with
  [GetRedrive](/queues-api/GetRedrive/) until `status` is `done` and `finishedAt` is set;
  [ListRedrives](/queues-api/ListRedrives/) lists a DLQ's redrives, newest first.
- A DLQ runs one redrive at a time: another answers `409 redrive_active`.

With the CLI: `ezgh queues peek orders-dlq` to look at its messages,
`ezgh queues redrive start orders-dlq [--to <queue>]` to move them, and
`ezgh queues redrive get orders-dlq <redrive-id>` to follow one.

To get rid of a DLQ's messages instead, [purge it](/queues-api/PurgeQueue/).

Source: https://docs.ezghcloud.com/queues/dead-letter-queues/index.mdx
