RabbitMQ prefetch count: unacked hoarding versus idle consumers

The basic.qos prefetch count is the most consequential consumer-side setting in RabbitMQ, and the one most teams never touch. The default is unlimited. That default is why a single slow consumer can pin thousands of messages in RAM while every other consumer on the same queue sits idle, and why “we added more consumers but nothing got faster” is such a common report.

Prefetch has two opposite failure modes, diagnosed with different signals. Set too high, one consumer hoards unacknowledged messages that are pinned in broker memory and invisible to other consumers. Set too low, consumers spend most of their time waiting for the next delivery instead of processing. This guide covers the mechanism, both failure modes, and how to pick a defensible number.

What prefetch actually controls

Prefetch is a per-consumer credit limit. When a consumer subscribes with basic.consume, the broker pushes up to prefetch_count messages to that consumer without waiting for acknowledgements. Every ack frees one slot, and the broker immediately delivers one more message to fill it.

Three properties of this mechanism drive everything else:

  • Unacked messages are pinned in memory. The broker must hold every unacked message available for instant redelivery if the consumer dies. Unlike ready messages, unacked messages cannot be paged to disk. Memory usage grows linearly with unacked count x message size, a direct precursor to the memory alarm, which blocks all publishers cluster-wide.
  • A hoarded message is a stalled message. While a message sits in one consumer’s prefetch buffer, no other consumer can process it. If that consumer is slow, stuck, or deadlocked, every message it holds is frozen until it acks, nacks, times out, or its channel closes.
  • Channel close triggers a mass requeue. When a consumer’s channel or connection dies, all of its unacked messages return to the queue in one burst. A consumer holding 10,000 unacked messages creates a 10,000-message redelivery spike when it disconnects.

Prefetch has no effect on consumers using auto-ack mode (there is nothing to wait for) or on basic.get pull consumers (each get is an explicit request). If your workload uses either, this knob does not apply.

The two failure modes

flowchart TD
  A[Prefetch count] --> B[Too high or unlimited]
  A --> C[Too low]
  B --> D[One consumer hoards thousands of unacked messages]
  D --> E[RAM pinned, other consumers idle]
  E --> F["Tell: unacked / (ready + unacked) > 0.8"]
  C --> G[Consumer acks, then waits for next delivery]
  G --> H[Throughput capped by ack round-trip]
  H --> I["Tell: consumer_utilisation < 1.0 with backlog"]

Failure mode 1: unacked hoarding

With unlimited or very high prefetch, the broker delivers messages to whichever consumer has capacity as fast as TCP allows. The first consumer to connect, or the one on the fastest network path, drains the queue into its own buffer. The queue looks almost empty in the management UI, but the work is not done: it is sitting, unacknowledged, inside one consumer.

The tell is the ratio messages_unacknowledged / (messages_ready + messages_unacknowledged). Above roughly 0.8, the vast majority of the queue’s work is in flight rather than awaiting delivery. Combined with consumer_utilisation below 1.0 and a low ack rate relative to the deliver rate, you have a hoarding consumer.

A second sanity check: unacked count on a queue should not exceed prefetch_count x active consumer count. If it does, you likely have a dead consumer whose channel never closed cleanly.

The failure escalates in three ways:

  1. Memory pressure. Unacked messages hold RAM on the broker. Sustained hoarding is one of the most common precursors to the memory alarm, at which point every publisher in the cluster is blocked.
  2. Head-of-line blocking. If the hoarding consumer hits a poison message or a slow downstream dependency, the thousands of messages it holds are stuck behind that one problem, even though other consumers are idle and healthy.
  3. Consumer timeout. RabbitMQ’s delivery acknowledgement timeout (consumer_timeout, default 30 minutes) closes the channel of a consumer that holds a delivery too long. A consumer that hoards a large batch and processes it sequentially will eventually trip this, mass-requeueing everything it held. See RabbitMQ consumer timeout: delivery acknowledgement timed out and the channel is closed.

Failure mode 2: idle consumers

With prefetch set very low, especially to 1, the consumer processes a message, sends the ack, and then waits. The broker only sends the next message after the ack arrives, so every message costs at least one network round trip of dead time.

The arithmetic is brutal on high-latency paths. If the round trip between broker and consumer is 125 ms and processing takes 5 ms, a prefetch of 1 means the consumer is busy 5 ms out of every 130 ms: idle about 96% of the time. Adding consumers does not fully compensate, because each one has the same serialization.

The tell here is consumer_utilisation persistently below 1.0 on a queue that has a backlog (messages_ready > 0), combined with a deliver rate well below what the queue could sustain. The queue has work, consumers are attached, and throughput is capped by the ack round trip. See RabbitMQ consumer_utilisation low: consumers attached but not keeping up for the broader diagnostic.

consumer_utilisation on an empty queue is not meaningful. A queue with nothing to deliver can show any utilisation value; only interpret it alongside backlog.

Sizing prefetch: the working method

There is no universal number, but there is a bounded method. You are balancing two constraints.

Throughput floor: cover one round trip. To keep a consumer busy continuously, its prefetch buffer must hold at least as many messages as it can process during one broker-to-consumer round trip plus ack time:

prefetch_min ~= (round_trip_time + processing_time) / processing_time

With a 125 ms round trip and 5 ms processing, that is about 26. With a 1 ms round trip on a local network and 50 ms processing, prefetch of 2 or 3 saturates the consumer. Consumers in the same datacenter as the broker need small values; consumers across regions or the public internet need larger ones.

Memory ceiling: cap by message size. The worst-case memory a single consumer can pin on the broker is prefetch_count x max message size. With 1 MB messages and prefetch 10,000, that is 10 GB held hostage by one consumer. Divide your acceptable per-consumer memory budget by your maximum message size to get a hard ceiling:

prefetch_max ~= memory_budget_per_consumer / max_message_size

Pick a value between the floor and the ceiling. Practical notes:

  • RabbitMQ maintainers have stated that prefetch values above a few hundred make virtually no difference to consumer throughput. Past that point you are buying risk, not speed.
  • Prefetch of 1 is correct when fairness matters more than throughput. If messages have highly variable processing times and you want strict work distribution, prefetch 1 (or a small single-digit value) is the right choice. Accept the throughput cost as the price of even load.
  • If processing time varies wildly within one queue, a large prefetch concentrates the slow tail on whichever consumer grabbed it. Prefer smaller prefetch and more consumers over large prefetch and few consumers in that case.
  • Measure the actual round trip. Do not guess from region names; cross-AZ traffic inside one cloud region can already be meaningful relative to fast processing.

Enforcing a sane default

Because the broker default is unlimited, every client that forgets to set prefetch gets the dangerous behavior. RabbitMQ offers a broker-side default via the default_consumer_prefetch setting, configured in advanced.config as {rabbit, [{default_consumer_prefetch, {false, 250}}]}. This applies a per-consumer limit of 250 to consumers that do not set their own; clients that set prefetch explicitly still override it. It is a useful safety net on shared clusters where you do not control every client library configuration.

Version and queue-type differences that change the answer

  • Global QoS is deprecated. The global flag on basic.qos applies one shared prefetch limit across all consumers on a channel. It was deprecated in RabbitMQ 4.0, and in a later release it moved to denied_by_default, meaning basic.qos with global=true is rejected unless the deprecated feature is explicitly re-enabled. Client libraries that call global QoS during connection autorecovery can fail to recover after a disconnect. Use per-consumer QoS (global=false) exclusively.
  • Quorum queues cap unacked messages per consumer. Even with prefetch set to 0 (unlimited), a quorum queue will not deliver more than a bounded number of unacked messages to a single consumer. Operators migrating from classic queues who relied on unlimited prefetch for batch processing will hit this ceiling; the maintainers’ recommendation for that pattern is streams.
  • Classic mirrored queues are gone. They were removed in RabbitMQ 4.0, so any prefetch behavior you observed on mirrored queues no longer applies after migration to quorum queues.
  • Consumer timeout gives hoarders a hard clock. The delivery acknowledgement timeout (consumer_timeout, default 30 minutes) closes the channel of a consumer that holds a delivery too long. A large prefetch combined with slow per-message processing turns this timeout from a safety net into a recurring channel-churn event.

Signals to watch in production

SignalWhy it mattersWarning sign
messages_unacknowledged per queueDirect measure of in-flight work pinned in RAMGrowing without bound; or steady at prefetch x consumers with a low ack rate
unacked / (ready + unacked) ratioDistinguishes hoarding from backlogSustained above 0.8 means nearly all work is in flight
consumer_utilisation per queueWhether attached consumers can accept deliveriesBelow 1.0 with messages_ready > 0 means consumers are the bottleneck
Ack rate vs deliver rateConfirms processing completesDeliver high, ack near zero: consumers receive but never finish
Deliver rate vs publish rateWhether the queue drainsDeliver capped well below what backlog should allow: prefetch too low
Redeliver rateMass requeues from dead or timed-out consumersSpikes correlated with consumer disconnects or timeout channel closures
mem_used / mem_limitUnacked hoarding ends hereRatio climbing in step with unacked growth

Correlate these, do not read them in isolation. A queue with 50,000 ready messages and utilisation 1.0 is a capacity problem (add consumers). A queue with 200 ready, 40,000 unacked, and utilisation 0.3 is a hoarding problem (fix prefetch and the stuck consumer). Same total depth, opposite fixes.

How Netdata helps

  • Tracks messages_ready and messages_unacknowledged as separate per-queue series, so the hoarding ratio is visible at a glance instead of requiring mental arithmetic on the management UI.
  • Collects per-queue message rates (publish, deliver, ack, redeliver), which is what you need to distinguish “consumers stuck” from “prefetch too low” from “queue simply overloaded”.
  • Correlates queue-level unacked growth with node memory usage on one dashboard, so you can see hoarding turning into memory pressure before the memory alarm fires.
  • Exposes consumer_utilisation per queue alongside depth, making the idle-consumer pattern (backlog plus low utilisation) directly observable.
  • Per-second granularity catches the requeue burst when a hoarding consumer’s channel closes, which slower polling intervals smooth into invisibility.