Consumer Acknowledgements (ack/nack)
IntermediatebasicAck signals successful processing; basicNack/basicReject requeues or discards the message. Unacknowledged messages are redelivered if the consumer disconnects.
Overview
Consumer acknowledgements are the mechanism RabbitMQ uses to know whether a message was processed successfully. Until a consumer acknowledges (or rejects) a message, RabbitMQ keeps the message in an "unacknowledged" state and holds it in memory. If the consumer connection drops without an ack, RabbitMQ requeues the message automatically for another consumer — at-least-once delivery. Three ack methods exist: basicAck (success, message deleted), basicNack (failure, optional requeue), and basicReject (single-message reject, optional requeue). The acknowledge mode can be automatic (broker deletes on delivery — fire-and-forget) or manual (safest). Combined with a prefetch count, manual acking is the foundation of reliable message processing in RabbitMQ.
basicAck, basicNack, basicReject — When to Use Each
Three ack primitives:
**basicAck(deliveryTag, multiple)** — acknowledge one or all unacked messages up to deliveryTag. Use after successful processing.
**basicNack(deliveryTag, multiple, requeue)** — negative-ack; requeue=true puts the message back at the front of the queue for retry; requeue=false routes it to the DLX (if configured) or discards it.
**basicReject(deliveryTag, requeue)** — same as basicNack but single-message only (no multiple flag).
Critical rule: a poisoned message that always fails processing will loop forever if you requeue=true. Route it to a DLQ after a max-retry count.
Channel channel = connection.createChannel();
// Set prefetch — limits unacked messages to 1 (fair dispatch)
channel.basicQos(1);
// Consume with manual ack (autoAck = false)
channel.basicConsume("order-queue", false, (consumerTag, delivery) -> {
long deliveryTag = delivery.getEnvelope().getDeliveryTag();
try {
Order order = deserialize(delivery.getBody());
orderService.process(order);
// SUCCESS — ack the message; broker removes it
channel.basicAck(deliveryTag, false);
} catch (TransientException e) {
// Transient failure — requeue for retry
channel.basicNack(deliveryTag, false, true);
} catch (PoisonMessageException e) {
// Permanent failure — do NOT requeue; routes to DLX if configured
channel.basicNack(deliveryTag, false, false);
}
}, consumerTag -> {});Auto-Ack vs Manual-Ack
Auto-ack (autoAck=true in basicConsume): the broker removes the message from the queue the moment it is delivered to the consumer — before the consumer processes it. If the consumer crashes mid-processing, the message is lost permanently. Use only for non-critical, idempotent, or fire-and-forget workloads.
Manual-ack (autoAck=false): the consumer explicitly calls basicAck or basicNack after processing. If the connection drops before acking, RabbitMQ requeues the message. This is the safe default for all production workloads. Pair with prefetch=1 for fair dispatch — the broker only sends the next message when the current one is acked.
# Auto-ack — dangerous, message lost if consumer crashes
channel.basicConsume("queue", true, deliverCallback, cancelCallback);
# Message is deleted on delivery — no recovery if crash during processing
# Manual-ack — safe, message requeued on consumer crash
channel.basicConsume("queue", false, deliverCallback, cancelCallback);
# Message stays unacked until basicAck or basicNack is called
# application.yml — Spring AMQP manual ack
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: MANUAL # or AUTO (auto-ack) or NONE (no-ack)
prefetch: 10 # 10 unacked messages max per consumerSpring AMQP — @RabbitListener with Manual Ack
In Spring AMQP, set acknowledge-mode: MANUAL and inject Channel + @Header(AmqpHeaders.DELIVERY_TAG) into the listener to call ack/nack manually. A simpler approach is to use the Channel parameter directly.
@Component
public class OrderConsumer {
@RabbitListener(queues = "order-queue")
public void onOrder(
Order order,
Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
try {
orderService.process(order);
channel.basicAck(deliveryTag, false); // success
} catch (TemporaryException e) {
// Requeue — will be retried
channel.basicNack(deliveryTag, false, true);
} catch (Exception e) {
// Dead-letter — do not requeue
log.error("Unrecoverable error processing order", e);
channel.basicNack(deliveryTag, false, false);
}
}
}Key Points to Remember
- 1Manual-ack (autoAck=false) is the safe default: messages are requeued if the consumer crashes before acking.
- 2basicAck → processed successfully; basicNack(requeue=true) → retry; basicNack(requeue=false) → dead-letter or discard.
- 3Never requeue=true for a message that always fails — it creates an infinite poison-message loop; route to DLQ instead.
- 4Set prefetch (basicQos) together with manual ack for fair dispatch: each consumer only receives the next message after acking the previous one.
- 5Auto-ack deletes the message on delivery — before processing — acceptable only for non-critical fire-and-forget scenarios.
- 6In Spring AMQP, set acknowledge-mode: MANUAL and inject Channel into the @RabbitListener method to call ack/nack.
Interview Questions
Sign in to ask AriaWhat is the difference between basicAck and basicNack in RabbitMQ?
What happens to an unacknowledged message when the consumer connection drops?
Why is auto-ack dangerous for critical message processing?
A message is nacked with requeue=true. What happens if it always fails processing?
How would you implement a max-retry policy before routing a message to a DLQ in RabbitMQ?
Ask Aria about Consumer Acknowledgements (ack/nack)
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.