Consumer Acknowledgements and Prefetch
AUTO vs MANUAL acknowledgement, ack/nack/reject and requeue semantics, prefetch and concurrency, and the throughput vs safety trade-off.
Prerequisite:First Producer and ConsumerYou’ll need: The working producer and consumer from the previous module.
Acknowledgements decide when the broker is allowed to forget a message, and prefetch decides how many in-flight messages a consumer holds. Together they control the safety and throughput of every consumer you write. This module is the canonical reference for both.
What you’ll be able to do after this module
- Explain AUTO vs MANUAL acknowledgement and when to use each.
- Use ack, nack, and reject with the correct requeue behavior.
- Set prefetch and concurrency to balance throughput against overload.
- Avoid the common pitfall of infinite requeue loops.
1. What an acknowledgement is
An acknowledgement is the consumer telling the broker it is done with a message. Until the broker receives an ack, it keeps the message and will redeliver it if the consumer disconnects. This is what makes delivery at least once, as introduced in Core Concepts.
sequenceDiagram
participant Broker as RabbitMQ
participant Consumer
Broker->>Consumer: deliver message (tag 42)
alt processed successfully
Consumer->>Broker: basicAck(42)
Note over Broker: message removed
else processing failed
Consumer->>Broker: basicNack(42, requeue=?)
Note over Broker: requeue or dead-letter
end
2. AUTO vs MANUAL acknowledgement mode
Spring AMQP offers three acknowledge modes, set with spring.rabbitmq.listener.simple.acknowledge-mode.
| Mode | Behavior | Use when |
|---|---|---|
| AUTO (default) | Spring acks after the listener returns normally, and nacks if it throws. | Most cases. Ties the ack to your method outcome. |
| MANUAL | You call ack or nack yourself on an injected Channel. | You need fine control, such as batching acks or acking only after an external commit. |
| NONE | The broker considers a message acked the moment it is delivered. | Rarely. Only when message loss is acceptable. |
Caution:
AcknowledgeMode.NONEmeans a crash mid-processing loses the message. Do not use it for work that matters.
AUTO mode (recommended default)
In AUTO mode you write plain business logic. Returning normally acks; throwing triggers a nack.
@RabbitListener(queues = "inventory.queue")
public void onOrderCreated(OrderCreated event) {
inventoryService.reserve(event); // throwing here nacks the message
}
MANUAL mode
Inject the Channel and delivery tag, then ack or nack explicitly.
@RabbitListener(queues = "inventory.queue")
public void onOrderCreated(OrderCreated event, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
try {
inventoryService.reserve(event);
channel.basicAck(tag, false);
} catch (Exception e) {
channel.basicNack(tag, false, false); // do not requeue; dead-letter instead
}
}
3. ack, nack, reject, and requeue
| Operation | Meaning |
|---|---|
basicAck(tag, multiple) | Confirm one message, or all up to this tag when multiple is true. |
basicNack(tag, multiple, requeue) | Reject one or many; requeue decides whether they return to the queue. |
basicReject(tag, requeue) | Reject exactly one message. |
The critical parameter is requeue:
requeue = trueputs the message back on the queue for another attempt. Useful for transient failures.requeue = falsediscards the message, or routes it to a dead-letter exchange if one is configured.
Caution: Requeuing on a deterministic failure creates an infinite loop: the message fails, requeues, and fails again forever, burning CPU. For errors that will always fail, do not requeue. Send the message to a dead-letter exchange instead, covered in the Reliability section. Structured retry with backoff is covered in the Retry and Error Handling module.
4. Prefetch (QoS)
Prefetch is the maximum number of unacknowledged messages the broker will deliver to a single consumer before waiting for acks. It is the main throughput and fairness control.
flowchart LR
Q["queue"]
subgraph c1 [Consumer 1]
direction TB
w1["prefetch = 2<br/>2 in flight"]
end
subgraph c2 [Consumer 2]
direction TB
w2["prefetch = 2<br/>2 in flight"]
end
Q --> c1
Q --> c2
Set it in application.yml:
spring:
rabbitmq:
listener:
simple:
prefetch: 20
Tuning guidance:
- Prefetch too high: one consumer grabs a large backlog, memory grows, and work is distributed unevenly across consumers.
- Prefetch too low (such as 1): strict fairness and low memory, but throughput suffers because the consumer waits for a round trip after each message.
- Start around 10 to 50 for fast handlers, and lower it for slow or memory-heavy handlers. Match prefetch to how much work a consumer can safely hold at once.
5. Concurrency
Prefetch controls in-flight messages per consumer. Concurrency controls how many consumer threads process the queue in parallel.
spring:
rabbitmq:
listener:
simple:
concurrency: 3 # minimum consumer threads
max-concurrency: 10 # scale up to this under load
prefetch: 20
Effective in-flight work is roughly concurrency x prefetch. Increase concurrency to use more cores and drain backlogs faster. Watch that downstream systems (a database, an external API) can handle the added parallelism.
Note: Ordering is only preserved within a single consumer thread. Raising concurrency processes messages in parallel, so do not rely on strict global order. Ordering trade-offs are covered in the Idempotency and Duplicates module.
6. Throughput vs safety
The two settings pull in opposite directions:
| Goal | Ack mode | Prefetch / concurrency |
|---|---|---|
| Maximum safety | AUTO or MANUAL, ack after success | Lower prefetch, modest concurrency |
| Maximum throughput | AUTO | Higher prefetch and concurrency |
The sound default for a Spring Boot service handling important work: AUTO ack, prefetch around 10 to 50, concurrency matched to your cores and downstream capacity, and a dead-letter exchange for messages that cannot be processed.
Checkpoint
You should now be able to:
- Choose AUTO or MANUAL acknowledgement for a listener.
- Use ack, nack, and reject with the right requeue flag.
- Explain why requeuing a deterministic failure loops forever.
- Tune prefetch and concurrency for throughput without overloading consumers.