확인 응답, 데드 레터 큐 및 재시도
수동 확인 응답으로 전달을 보장하고 처리할 수 없는 메시지를 다루며 재시도와 데드 레터 흐름을 구현합니다.
확인 응답, 데드 레터 큐 및 재시도은(는) CoddyKit의 무료 Node.js Backend Development Bootcamp 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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 messagechannel.nack(msg, false, requeue)— failure; ifrequeue=truethe message goes back to the queue, iffalseit 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 waitingPutting 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=falsehands 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-deathor a custom header) to cap retries before parking the message.
Together these patterns give you bounded, observable, lossless message processing.
자주 묻는 질문
“확인 응답, 데드 레터 큐 및 재시도” 강의는 무료인가요?
네 — “확인 응답, 데드 레터 큐 및 재시도” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Node.js Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
“확인 응답, 데드 레터 큐 및 재시도”에서 뭘 배우나요?
수동 확인 응답으로 전달을 보장하고 처리할 수 없는 메시지를 다루며 재시도와 데드 레터 흐름을 구현합니다. 브라우저에서 직접 실행하는 실습 코드로 Node.js Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Node.js Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Node.js Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“확인 응답, 데드 레터 큐 및 재시도” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Node.js Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Node.js Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 생산자, 소비자 및 AMQP 모델
- 교환기 유형: Direct, Topic, Fanout 및 Headers
- 확인 응답, 데드 레터 큐 및 재시도
- 작업 큐, 프리페치 및 경쟁 소비자