Publishing & Consuming Messages

A complete Node.js amqplib example publishing and consuming messages, with ack/nack semantics and prefetch.

Publishing a message

Using amqplib, the standard Node.js AMQP client:

Javascript
const amqp = require('amqplib');

async function publishOrderCreated(order) {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();

  const exchange = 'orders.topic';
  await channel.assertExchange(exchange, 'topic', { durable: true });

  const routingKey = `order.${order.region}.created`;
  const payload = Buffer.from(JSON.stringify(order));

  channel.publish(exchange, routingKey, payload, { persistent: true });

  await channel.close();
  await connection.close();
}

publishOrderCreated({ id: 'order-482', region: 'eu', total: 129.99 });

persistent: true is what makes this message survive a broker restart, provided the queue it lands in is also declared durable: true (see Exchanges, Queues & Bindings).

Consuming a message

Javascript
const amqp = require('amqplib');

async function consumeOrders() {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();

  const exchange = 'orders.topic';
  const queue = 'inventory.orders.queue';

  await channel.assertExchange(exchange, 'topic', { durable: true });
  await channel.assertQueue(queue, { durable: true });
  await channel.bindQueue(queue, exchange, 'order.*.created');

  channel.prefetch(10); // at most 10 unacknowledged messages in flight to this consumer at once

  channel.consume(queue, async (msg) => {
    if (msg === null) return; // consumer was cancelled by the server

    try {
      const order = JSON.parse(msg.content.toString());
      await reserveStock(order);
      channel.ack(msg);
    } catch (err) {
      const isRetryable = !(err instanceof ValidationError);
      channel.nack(msg, false, isRetryable); // requeue only if it's worth retrying
    }
  });
}

consumeOrders();

Acknowledgment semantics: ack and nack

Every message delivered to a consumer sits in an "unacknowledged" state until the consumer explicitly says what happened to it:

  • channel.ack(msg) — processing succeeded; the broker permanently removes the message from the queue.
  • channel.nack(msg, allUpTo, requeue) — processing failed. The second argument, allUpTo, lets you nack every unacked message up to and including this one in a single call (almost always false in practice). The third, requeue, decides the message's fate: true puts it straight back at the front of the queue for immediate redelivery (to this or another consumer); false drops it — or, if the queue has a dead-letter exchange configured, routes it there instead (see Reliability Patterns).

If a consumer's connection or channel dies with messages still unacknowledged, RabbitMQ automatically requeues them for another consumer — this is the mechanism that gives RabbitMQ its at-least-once delivery guarantee, exactly mirroring the trade-off described on the System Design track's Message Queues & Event-Driven Architecture page: a redelivered message means your processing must be idempotent.

Prefetch / QoS: why it matters with multiple consumers

Without a prefetch limit, RabbitMQ will push as many messages as it can to a fast, already-connected consumer, even while other consumers on the same queue sit idle — because by default there's no cap on how many unacknowledged messages one consumer can be holding at once. channel.prefetch(10) caps that at 10: once a consumer has 10 unacked messages outstanding, RabbitMQ stops sending it more until some of those are acked or nacked. With multiple consumers on the same queue, this is what actually makes round-robin-style load balancing across them work in practice, instead of one consumer hoarding the entire backlog.

Common mistakes

  • Never calling ack or nack at all — messages pile up as permanently "unacked," the queue looks stuck even though messages are technically still flowing in, and a consumer reconnect triggers a redelivery of the entire backlog at once.
  • Using { noAck: true } (auto-ack) for work where a failure partway through processing matters — the message is already gone from the broker's perspective before you know whether your code actually succeeded.
  • Skipping prefetch() entirely in a multi-consumer setup, so one greedy consumer receives most of the traffic while its siblings starve.
  • Calling nack(msg, false, true) (requeue) for an error that will never succeed no matter how many times it's retried — this creates a tight retry loop that burns CPU and floods logs instead of ever making progress.