0%
RabbitMQ Deep Dive: AMQP, Queues, and Delivery Guarantees
The Broker Model
Queues and Storage
Delivery Guarantees
Patterns
Prefetch and Consumer Throughput
Prefetch is a single small number that determines a consumer's throughput over two orders of magnitude, and the frame that sets it has three fields of which one is ignored, one is widely misunderstood, and only the third does what people think.
The three fields of the frame
Worth reading in order, because two of them are traps.
basic.qos carries prefetch-size as a 32-bit integer, prefetch-count as a 16-bit integer, and global as a single bit. prefetch-count is the number of unacknowledged messages the broker will allow outstanding, and it is the only field most code sets.
prefetch-size is a byte limit intended to bound the outstanding data rather than the outstanding count, and RabbitMQ does not implement it. A non-zero value is not an error and is not honoured, which means code carefully bounding memory with a byte limit is running with no limit at all. The field exists in the protocol and the broker ignores it.
The 16-bit width of prefetch-count is the other detail worth knowing: the maximum is 65,535, and a library that accepts a larger value is clamping or wrapping it somewhere. Zero has a special meaning rather than being the smallest value, and it means unlimited.
Zero, which means no limit at all
The default in several clients, and almost never the right value.
A prefetch-count of zero, or a consumer that never sends basic.qos, places no bound on outstanding deliveries. The broker pushes messages as fast as the connection carries them, and the consumer's socket buffer and client-library queue absorb however many that is.
The consequences are a memory footprint set by the queue's depth rather than by anything the consumer chose, and a redelivery blast radius equal to everything in flight. A client holding 200,000 messages in its internal buffer after a channel close requeues all of them, and until the requeue completes the queue's depth and its messages_unacknowledged are both large and both misleading.
It also makes the consumer invisible to load balancing. With no limit, the first consumer to connect receives essentially the whole queue, because the broker delivers until it runs out of messages rather than until it runs out of credit, and the consumers that connect afterwards sit idle against an empty ready set while the first one works through a backlog it holds privately.
The global flag, and RabbitMQ's interpretation
The field whose meaning differs from the specification.
In AMQP 0-9-1 as written, global true means the limit applies to the whole connection and false means it applies per consumer. RabbitMQ implements something different: global false means per consumer, and global true means per channel, with no connection-wide option at all.
The distinction matters as soon as a channel has more than one consumer. With global false and a prefetch of 10, each of three consumers on the channel gets its own window of 10, so 30 messages can be outstanding. With global true, the three share a single window of 10.
Per channel is the one people expect when they share a channel deliberately, because it bounds the whole channel's memory. Per consumer is the default in most client libraries and is usually what is wanted, because it makes the limit a property of a worker rather than of an accident of how channels were allocated. The combination that surprises is global false with many consumers per channel, where the effective limit is the prefetch times the consumer count.
When both limits are set
Legal, and the arithmetic is an intersection.
A channel may send basic.qos twice, once with global true and once with global false, and both limits then apply. A delivery requires credit under the per-consumer window and under the channel-wide window simultaneously, so the effective bound on any single consumer is the smaller of the two, and the channel's total is bounded by the global value regardless of how many consumers it carries.
That combination is the correct configuration for a channel serving several consumers where both properties matter: a per-consumer limit of 20 keeps any one worker from hoarding, and a channel limit of 100 caps the process's memory no matter how many consumers get registered. Neither limit alone does both jobs, which is why the two-call form exists rather than being a redundancy.
The trap is setting the second call and expecting it to replace the first. Sending global false with 20 and later global true with 100 leaves both in force, and a team that meant to switch to a channel-wide limit has a per-consumer one as well. A basic.qos call never clears the other scope, so moving from one to the other means setting the scope you are leaving to zero explicitly, which is the unlimited value and therefore disables it.
What basic.get does not obey
An exception that catches polling consumers.
Prefetch governs push deliveries, which are the ones the broker sends unsolicited to a registered consumer after basic.consume. basic.get is a synchronous poll, and it is not subject to basic.qos at all: each call returns at most one message and the broker will answer every call regardless of how many previous responses remain unacknowledged.
A consumer that polls in a loop and acknowledges lazily can therefore accumulate an unbounded unacknowledged set with a prefetch of 1 configured on its channel, which looks impossible from the configuration and is exactly what the protocol specifies. The only bound on a polling consumer is its own discipline.
This also explains a confusing pair of readings: a queue with a high messages_unacknowledged and a consumers count of zero. Polling does not register a consumer, so the queue reports none, and the outstanding deliveries came from basic.get calls that the prefetch never saw. Any discussion of tuning prefetch is irrelevant to that consumer, and the fix for a polling consumer is to stop polling, because the round-trip cost prefetch exists to amortise is unavoidable when every message is requested individually.
Where the limit is enforced
In the queue process, not in the channel.
Each queue tracks, per consumer, how many deliveries it has sent that remain unacknowledged, and stops sending to that consumer when the count reaches the prefetch. An acknowledgement decrements it and immediately frees a slot, which is why throughput is bounded by the round trip and not by the broker's capacity.
This is a credit mechanism, and the credit is held by the queue rather than requested by the consumer. The consumer cannot ask for more; it can only acknowledge, and the acknowledgement is simultaneously a statement about processing and a request for the next message. The ack is the flow-control signal, which is why autoAck removes flow control entirely rather than merely weakening it.
Because the accounting is per queue and per consumer, a consumer subscribed to three queues on one channel with global false has three independent windows. Its real in-flight ceiling is the prefetch times three, which is the usual explanation for a consumer whose memory use is a multiple of what its prefetch suggests.
Changing it on a live channel
Possible, with one restriction worth knowing.
basic.qos can be sent at any time on an open channel, and the new value applies to subsequent deliveries. Lowering it does not revoke messages already in flight: the broker simply stops sending until the outstanding count falls below the new limit, so a consumer dropping from 1,000 to 10 keeps its 1,000 and then receives nothing for a long time.
Raising it takes effect immediately, which makes it the safer direction to move during an incident. Dropping the prefetch on a struggling consumer group does nothing until the current windows drain, and the mitigation that looks instant takes as long as the current in-flight work, which for slow processing is minutes.
The restriction is ordering relative to basic.consume. Prefetch is a channel-and-consumer property evaluated when the consumer is registered, so sending basic.qos after basic.consume leaves a window during which the consumer is unlimited. On a reconnect against a deep queue that window is long enough to deliver the whole backlog, which is the reconnect bug described in the previous section arriving by a different route.
RabbitMQ does not implement prefetch-size. A non-zero value is accepted, produces no error, and is silently ignored, so a consumer that bounds its memory with a byte limit rather than a message count has no limit at all. The only effective control is prefetch-count, and bounding memory therefore means estimating the message size and converting it to a count.