0Pricing
Node.js Backend Development Bootcamp · レッスン

Acknowledgement、デッドレターキュー、リトライ

手動ackで配信を保証し、処理不能なメッセージに対処して、リトライとデッドレターのフローを実装します。

「Acknowledgement、デッドレターキュー、リトライ」はCoddyKit上の無料Node.js Backend Development Bootcampレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはNode.js Backend Development Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Node.js Backend Development Bootcampコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

Why Acknowledgements Matter

By default, when a consumer receives a message from RabbitMQ it must tell the broker whether the message was processed successfully. This signal is called an acknowledgement (ack).

If you enable auto-ack, RabbitMQ deletes a message the instant it is delivered. If your worker then crashes mid-processing, the message is gone forever. With manual acks, the broker keeps the message until you confirm it, and re-delivers it if the consumer dies.

  • auto-ack: fast, but at-most-once delivery (can lose messages)
  • manual ack: safe, gives you at-least-once delivery

For any job that does real work (charging a card, sending email, writing to a DB), you almost always want manual acks.

Consuming With Manual Acks

Using the amqplib library, you pass { noAck: false } to channel.consume to enable manual acknowledgement. After your handler finishes successfully, you call channel.ack(msg).

Until you ack, RabbitMQ considers the message unacked and will redeliver it if the channel or connection closes.

const amqp = require('amqplib');

async function start() {
  const conn = await amqp.connect('amqp://localhost');
  const channel = await conn.createChannel();
  const queue = 'orders';

  await channel.assertQueue(queue, { durable: true });

  channel.consume(queue, async (msg) => {
    if (msg === null) return;
    const order = JSON.parse(msg.content.toString());
    console.log('Processing order', order.id);
    // ... do real work here ...
    channel.ack(msg); // confirm success
  }, { noAck: false });
}

start();

ack, nack, and reject

RabbitMQ gives you three ways to respond to a delivered message:

  • channel.ack(msg) — success, remove the message
  • channel.nack(msg, false, requeue) — failure; if requeue=true the message goes back to the queue, if false it is discarded (or dead-lettered)
  • channel.reject(msg, requeue) — like nack but for a single message only

The second argument of nack is allUpTo. Set it to false to act only on the current message; true nacks every unacked message up to this one.

Key decision: requeue=true retries immediately and can cause infinite hot-loops for poison messages. Prefer requeue=false plus a dead-letter strategy.

// Failure with no requeue -> message is dropped or dead-lettered
channel.nack(msg, false, false);

// Failure with requeue -> message returns to the front of the queue
channel.nack(msg, false, true);

// reject only ever affects this one message
channel.reject(msg, false);

Prefetch: Controlling In-Flight Work

Without limits, RabbitMQ pushes as many messages as it can to a consumer, all sitting unacked in memory. That can overwhelm a slow worker.

channel.prefetch(n) sets the maximum number of unacknowledged messages a consumer may hold at once. With manual acks, a worker with prefetch(1) processes one message, acks it, then receives the next — giving fair, back-pressured dispatch.

const channel = await conn.createChannel();
await channel.assertQueue('orders', { durable: true });

// Only one unacked message at a time per consumer
await channel.prefetch(1);

channel.consume('orders', async (msg) => {
  await handle(msg);
  channel.ack(msg);
}, { noAck: false });

What Is a Poison Message?

A poison message is one that always fails processing — bad JSON, a reference to a deleted record, an unrecoverable validation error. If you simply nack(msg, false, true) it, the message requeues, gets redelivered, fails again, and loops forever, pinning your CPU.

You need a way to give up after a few tries and move the message somewhere safe for inspection. That somewhere is a Dead-Letter Queue (DLQ).

  • Stop retrying after N attempts
  • Move the message out of the main flow
  • Keep it for debugging / manual replay

Dead-Letter Exchanges

RabbitMQ can automatically route a message to another exchange when it is dead-lettered. A message is dead-lettered when:

  • it is nacked/rejected with requeue=false, or
  • its TTL expires, or
  • the queue exceeds its max length

You configure this with the queue arguments x-dead-letter-exchange (required) and optionally x-dead-letter-routing-key. Bind a queue to that exchange and dead messages land there.

// Main queue routes failures to the 'dlx' exchange
await channel.assertExchange('dlx', 'direct', { durable: true });
await channel.assertQueue('orders.dlq', { durable: true });
await channel.bindQueue('orders.dlq', 'dlx', 'orders.dead');

await channel.assertQueue('orders', {
  durable: true,
  arguments: {
    'x-dead-letter-exchange': 'dlx',
    'x-dead-letter-routing-key': 'orders.dead'
  }
});

Sending a Failure to the DLQ

Once the orders queue has a dead-letter exchange configured, dropping a message there is simply a matter of nacking without requeue.

The broker handles the routing for you — you never publish to the DLQ manually. This keeps your consumer logic clean: process, and on unrecoverable failure, let RabbitMQ dead-letter it.

channel.consume('orders', async (msg) => {
  try {
    const order = JSON.parse(msg.content.toString());
    await processOrder(order);
    channel.ack(msg);
  } catch (err) {
    console.error('Unrecoverable failure:', err.message);
    // requeue=false -> RabbitMQ dead-letters to orders.dlq
    channel.nack(msg, false, false);
  }
}, { noAck: false });

Counting Retries With Headers

Often you want to retry a few times before giving up — a transient DB timeout might succeed on the second try. RabbitMQ has no built-in retry counter, so you track attempts yourself.

When a message is dead-lettered, RabbitMQ adds an x-death header array describing each dead-letter event, including a count. You can read it to decide whether to retry or to route to the final DLQ.

function deathCount(msg) {
  const xDeath = msg.properties.headers && msg.properties.headers['x-death'];
  if (!Array.isArray(xDeath) || xDeath.length === 0) return 0;
  // sum counts across dead-letter events for this reason
  return xDeath.reduce((sum, d) => sum + (d.count || 0), 0);
}

const attempts = deathCount(msg);
if (attempts >= 3) {
  channel.nack(msg, false, false); // give up -> parking DLQ
} else {
  // route back for another attempt
}

Delayed Retries With a TTL Wait Queue

Immediate requeue retries instantly, which is bad for transient errors that need time to clear. A common pattern is a retry queue with a TTL that dead-letters back to the main queue.

Flow: main queue fails → message goes to orders.retry which has x-message-ttl (e.g. 5s) and a dead-letter exchange pointing back to orders. After the TTL expires, RabbitMQ moves it back automatically — giving a built-in delay between attempts.

// Retry queue: holds messages for 5s, then dead-letters back to main
await channel.assertQueue('orders.retry', {
  durable: true,
  arguments: {
    'x-message-ttl': 5000,
    'x-dead-letter-exchange': '',          // default exchange
    'x-dead-letter-routing-key': 'orders'  // back to main queue
  }
});

// On a retryable failure, publish into the wait queue instead of nacking
channel.sendToQueue('orders.retry', msg.content, {
  persistent: true,
  headers: msg.properties.headers
});
channel.ack(msg); // ack the original; the copy is now waiting

Putting the Retry Logic Together

Here is the decision flow a robust consumer follows on failure:

  • Parse/validate fails permanently → dead-letter to the parking DLQ immediately
  • Transient error and attempts < max → push to the retry (TTL) queue for a delayed re-attempt
  • Transient error but attempts ≥ max → give up, send to the parking DLQ

This bounds total work, avoids hot-loops, and preserves failed messages for inspection or manual replay.

const MAX_RETRIES = 3;

channel.consume('orders', async (msg) => {
  let order;
  try {
    order = JSON.parse(msg.content.toString());
  } catch (e) {
    return channel.nack(msg, false, false); // poison -> parking DLQ
  }

  try {
    await processOrder(order);
    channel.ack(msg);
  } catch (err) {
    const attempts = retryHeader(msg);
    if (attempts >= MAX_RETRIES) {
      channel.nack(msg, false, false);    // exhausted -> parking DLQ
    } else {
      channel.sendToQueue('orders.retry', msg.content, {
        persistent: true,
        headers: { ...msg.properties.headers, 'x-retries': attempts + 1 }
      });
      channel.ack(msg);                    // ack original; retry is queued
    }
  }
}, { noAck: false });

A Runnable Retry Simulation

The RabbitMQ behavior needs a broker, but the retry decision logic is plain JavaScript you can reason about and test in isolation. Below is a self-contained simulation of the ack / retry / dead-letter decision an order consumer makes.

It demonstrates the core idea: succeed and ack, retry while under the limit, and park to the DLQ once retries are exhausted.

const MAX_RETRIES = 3;

function decide(message) {
  // simulate processing: 'good' succeeds, 'bad-json' is poison, else transient
  if (message.kind === 'good') return { action: 'ack' };
  if (message.kind === 'bad-json') return { action: 'dlq', reason: 'poison' };

  const attempts = message.retries || 0;
  if (attempts >= MAX_RETRIES) return { action: 'dlq', reason: 'exhausted' };
  return { action: 'retry', retries: attempts + 1 };
}

const inbox = [
  { id: 1, kind: 'good' },
  { id: 2, kind: 'bad-json' },
  { id: 3, kind: 'transient', retries: 0 },
  { id: 4, kind: 'transient', retries: 3 }
];

for (const msg of inbox) {
  const result = decide(msg);
  console.log('msg ' + msg.id + ' ->', JSON.stringify(result));
}

Quick Check

You have a consumer using manual acks. A message contains malformed JSON that can never be parsed. You want to stop it from looping forever while keeping it for later inspection.

Recap

You learned how to make RabbitMQ delivery reliable and resilient:

  • Manual acks (noAck: false + channel.ack) give at-least-once delivery so a crash never loses a message.
  • nack/reject with requeue=false hands a failed message to a dead-letter exchange instead of looping forever.
  • prefetch(n) back-pressures slow workers by capping unacked messages.
  • Dead-letter exchanges (x-dead-letter-exchange) auto-route failures to a DLQ for inspection and replay.
  • Retries use a TTL wait queue that dead-letters back to the main queue, with an attempt counter (via x-death or a custom header) to cap retries before parking the message.

Together these patterns give you bounded, observable, lossless message processing.

よくある質問

「Acknowledgement、デッドレターキュー、リトライ」レッスンは無料ですか?

はい。「Acknowledgement、デッドレターキュー、リトライ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Node.js Backend Development Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Node.js Backend Development Bootcampコースには全4レッスンが含まれています。

「Acknowledgement、デッドレターキュー、リトライ」で何を学びますか?

手動ackで配信を保証し、処理不能なメッセージに対処して、リトライとデッドレターのフローを実装します。 ブラウザで直接実行するハンズオンコードでNode.js Backend Development Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Node.js Backend Development Bootcampを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのNode.js Backend Development Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「Acknowledgement、デッドレターキュー、リトライ」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このNode.js Backend Development Bootcampレッスンでコードを書いて実行できますか?

はい。すべてのNode.js Backend Development Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. プロデューサー、コンシューマー、AMQPモデル
  2. エクスチェンジの種類:Direct、Topic、Fanout、Headers
  3. Acknowledgement、デッドレターキュー、リトライ
  4. ワークキュー、プリフェッチ、競合コンシューマー
← Node.js Backend Development Bootcampに戻る