Rabbit Relay

Error Handling

Consumer failure behavior is explicit, choose what happens to the message when a handler throws.

Rabbit Relay makes consumer failure behavior explicit.

When a handler throws, you choose what happens to the message.


RabbitMQ acknowledgements

RabbitMQ uses acknowledgements to know whether a delivered message has been handled successfully.

TermMeaning
ACKThe message was handled and RabbitMQ can remove it from the queue
NACK requeue=trueThe message was rejected and should be put back on the queue
NACK requeue=falseThe message was rejected and should not be requeued
Dead-letterRabbitMQ routes a rejected message to a configured DLQ

Rabbit Relay default

If a handler succeeds, Rabbit Relay ACKs the message. If a handler throws, Rabbit Relay follows the onError policy.


Error modes

await sub.consume({
  onError: "ack" | "requeue" | "dead-letter" | "retry",
});
onErrorRabbitMQ behaviorUse case
"ack"ACK even after handler errorNon-critical events, logs, metrics
"requeue"NACK requeue=trueTransient failures, use carefully
"dead-letter"NACK requeue=falsePoison messages, validation errors
"retry"Publish retry copy, then ACK originalProduction consumers

ack (default)

await sub.consume({
  onError: "ack",
});

Behavior:

  • handler errors are logged
  • message is acknowledged
  • no retry occurs

Use for non-critical events, logs, metrics, or handlers where failure should not block the queue.

Data loss risk

onError: "ack" drops failed messages. Use it only when that is acceptable.


requeue

await sub.consume({
  onError: "requeue",
});

Behavior:

  • message is negatively acknowledged
  • message is requeued
  • RabbitMQ may redeliver it immediately

Infinite loop risk

Do not use requeue as your main retry strategy. If the error is not transient, the same message can be delivered repeatedly forever.


dead-letter

await sub.consume({
  onError: "dead-letter",
});

Behavior:

  • message is negatively acknowledged
  • requeue is disabled
  • RabbitMQ routes the message to the configured DLQ

Use for poison messages, validation errors, and failures that need inspection.


retry

await sub.consume({
  onError: "retry",
  retry: {
    attempts: 3,
    then: "dead-letter",
  },
});

Behavior:

  • Rabbit Relay republishes the message for another attempt
  • retry metadata is stored in headers
  • after max attempts, the final behavior is applied

Common final behavior:

then: "dead-letter"

Production default

For production consumers, prefer bounded retry followed by DLQ.


Why Rabbit Relay ACKs the original after retry publish

When retry is enabled, Rabbit Relay does not leave the original message unacked forever.

Instead:

  1. It republishes a retry copy with retry headers
  2. It ACKs the original only after the retry copy is published
  3. RabbitMQ later delivers the retry copy

This avoids infinite immediate redelivery loops and keeps retry metadata explicit.


const sub = await broker
  .queue("orders.q")
  .exchange("orders.ex", {
    exchangeType: "topic",
    routingKey: "orders.*",
    deadLetter: {
      exchange: "orders.dlx",
      queue: "orders.dlq",
      routingKey: "orders.dead",
      autoDeclare: true,
    },
  });

await sub.consume({
  prefetch: 20,
  concurrency: 5,
  onError: "retry",
  retry: {
    attempts: 3,
    then: "dead-letter",
  },
});

This gives you:

  • bounded retries
  • no infinite requeue loop
  • failed messages isolated in a DLQ
  • visible retry metadata

At-least-once delivery

RabbitMQ delivery remains at-least-once.

A message can be delivered more than once if:

  • a consumer crashes before ACK
  • the connection drops during processing
  • a retry or redrive publishes another copy
  • a publisher retries after an uncertain failure

Design handlers to be idempotent

Use stable IDs, unique constraints, de-duplication, or idempotent writes where duplicate processing would be harmful.


Summary

  • ack drops failed messages
  • requeue sends them back immediately
  • dead-letter parks them in a DLQ
  • retry retries a bounded number of times
  • Prefer retry + DLQ for production consumers

On this page