Skip to content

Dead-letter queues

Move messages that keep failing to a dead-letter queue, inspect them without consuming them, and redrive them back.

Updated View as Markdown

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:

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 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 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.

curl -sS "$BASE/queues/$DLQ_ID/messages?limit=20" -H "Authorization: Bearer $EZGH_API_KEY"
{
  "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 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 on the dead-letter queue moves its messages back:

curl -sS -X POST "$BASE/queues/$DLQ_ID/redrives" \
  -H "Authorization: Bearer $EZGH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
{
  "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 until status is done and finishedAt is set; 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close