RabbitMQ Message Acknowledgment Explained

January 6, 20264 min readRabbitMQ tutorial

RabbitMQ Message Acknowledgment Explained

Why acknowledgments matter

When RabbitMQ delivers a message to a consumer, it needs to know when it's safe to remove that message from the queue. Without acknowledgments, RabbitMQ has two bad options: remove the message immediately (risking data loss if the consumer crashes) or never remove it (causing infinite redelivery).

Acknowledgments solve this by letting consumers tell RabbitMQ: "I'm done with this message, you can delete it."

Automatic acknowledgment (noAck)

With automatic acknowledgment, RabbitMQ removes the message from the queue as soon as it's delivered to the consumer over the network. The broker doesn't wait for the consumer to finish processing.

channel.consume("orders", (msg) => {
  processOrder(msg);
}, { noAck: true });

This is the simplest mode, but it's risky:

  • If the consumer crashes mid-processing, the message is gone forever
  • If the consumer can't keep up, messages pile up in memory (no backpressure)
  • There's no way to tell RabbitMQ that processing failed

Use noAck: true only when losing messages is acceptable, like for non-critical metrics or logging where occasional data loss doesn't matter.

Manual acknowledgment (ack)

With manual acknowledgment, the consumer explicitly tells RabbitMQ when a message has been successfully processed. The message stays in the queue (marked as "unacknowledged") until the consumer sends an ack.

channel.consume("orders", (msg) => {
  try {
    processOrder(msg);
    channel.ack(msg);
  } catch (err) {
    console.error("Processing failed", err);
  }
});

If the consumer disconnects without acknowledging, RabbitMQ automatically requeues the message and delivers it to another consumer (or the same one when it reconnects).

This is the recommended mode for any production workload.

Negative acknowledgment (nack)

When processing fails and you want to explicitly tell RabbitMQ, use nack. It gives you control over what happens to the failed message:

Nack and requeue

Reject the message and put it back at the front of the queue for redelivery:

channel.consume("orders", (msg) => {
  try {
    processOrder(msg);
    channel.ack(msg);
  } catch (err) {
    channel.nack(msg, false, true);
  }
});

The third argument (true) means "requeue this message." Be careful with this pattern: if the message fails every time, it creates an infinite loop of redelivery. Always combine requeue with a retry strategy to avoid this.

Nack and discard

Reject the message and remove it from the queue permanently:

channel.nack(msg, false, false);

If the queue has a dead-letter exchange configured, the message is routed there instead of being discarded. This is the recommended approach for handling poison messages.

Bulk nack

The second argument to nack controls whether it applies to multiple messages. When set to true, it negatively acknowledges all unacknowledged messages up to and including the current one:

channel.nack(msg, true, false);

This is useful when a batch of messages depends on a shared resource that becomes unavailable.

Reject

reject works like nack but only for a single message (no bulk option):

channel.reject(msg, true);   // reject and requeue
channel.reject(msg, false);  // reject and discard (or dead-letter)

nack was introduced later as an extension to AMQP and is generally preferred since it supports bulk operations.

Prefetch and acknowledgments

Prefetch count controls how many unacknowledged messages RabbitMQ will deliver to a consumer at once. Without it, RabbitMQ sends messages as fast as it can, which can overwhelm the consumer.

await channel.prefetch(10);
 
channel.consume("orders", (msg) => {
  processOrder(msg);
  channel.ack(msg);
});

With a prefetch of 10, RabbitMQ delivers up to 10 messages and then waits until some are acknowledged before sending more. This creates natural backpressure and ensures no single consumer hogs all the work.

Choosing a prefetch value

  • Too low (1–2): safe but slow, each message must be fully processed before the next one arrives
  • Too high (1000+): fast but risky, consumer memory spikes if processing is slow
  • Good starting point: 10–50, then tune based on your consumer's processing time

The redelivered flag

When a message is requeued (via nack, reject, or consumer disconnect), RabbitMQ sets the redelivered flag to true. You can use this to detect messages that have been delivered before:

channel.consume("orders", (msg) => {
  if (msg.fields.redelivered) {
    console.warn("This message was redelivered");
  }
  processOrder(msg);
  channel.ack(msg);
});

This flag is useful for logging and debugging, but don't rely on it as a retry counter — it only tells you the message has been delivered more than once, not how many times.

Common pitfalls

Forgetting to ack

If you consume with manual acknowledgment but never call ack, nack, or reject, messages stay in the "unacknowledged" state. Once the prefetch limit is reached, RabbitMQ stops delivering new messages to that consumer. The queue appears to stall.

Acking twice

Acknowledging the same message twice throws a channel error and closes the channel. Always ensure your ack/nack logic runs exactly once per message.

Requeue loops

Nacking with requeue (channel.nack(msg, false, true)) on a message that always fails creates an infinite redelivery loop. The message bounces between the queue and the consumer forever, burning CPU and blocking other messages. Use a retry pattern with a maximum retry count to prevent this.

Long-running consumers without heartbeats

If your consumer takes a long time to process a message and the TCP connection times out, RabbitMQ assumes the consumer is dead and requeues all unacknowledged messages. Make sure your connection heartbeat interval is longer than your longest processing time, or break long tasks into smaller chunks.

Monitoring unacknowledged messages

A spike in unacknowledged messages usually means consumers are struggling. Use RabbitGUI to monitor the unacknowledged message count per queue in real time and identify bottlenecks before they cascade into broader issues.

Read more RabbitMQ tutorials

RabbitMQ streams explainedRabbitMQ tutorialRabbitMQ streams explainedLearn what RabbitMQ Streams are, how they differ from traditional queues, and when to use them for high-throughput event streaming, replay, and fan-out.RabbitMQ default port and port configurationRabbitMQ tutorialRabbitMQ default port and port configurationA comprehensive guide on RabbitMQ default ports, what they are used for, and how to configure them for your RabbitMQ instances.What Is RabbitMQ?RabbitMQ tutorialWhat Is RabbitMQ?Learn what RabbitMQ is, how it works, and why it’s used in modern software architectures. Discover RabbitMQ’s benefits, key components, use cases, and how it enables reliable asynchronous communication.

RabbitGUI, the missing RabbitMQ IDE

Debug, monitor, and manage RabbitMQ with a modern developer interface.

Try nowRabbitGUI screenshot

More articles about RabbitMQ

RabbitMQ Javascript Cheat-SheetCheat sheetRabbitMQ Javascript Cheat-SheetEverything you need to know to get started with RabbitMQ in NodeJs and Docker with code examples ready to go.How to log into your CloudAMQP RabbitMQ instanceProductHow to log into your CloudAMQP RabbitMQ instanceUse RabbitGUI to connect to your CloudAMQP instance and manage your dead letter queues with easeHow security is built into RabbitGUIProductHow security is built into RabbitGUIRabbitGUI was built with security as a top priority for its users, and here is how it was done!