Mastering RabbitMQ Prefetch Settings for Optimal Consumer Performance

Tune RabbitMQ prefetch so consumers stay busy without hoarding messages or hiding slow processing.

Mastering RabbitMQ Prefetch Settings for Optimal Consumer Performance

RabbitMQ prefetch is one of those settings that looks tiny and changes everything. It controls how many unacknowledged messages RabbitMQ will allow a consumer to hold at once. Set it too low and fast consumers spend too much time waiting for the next delivery. Set it too high and slow consumers quietly hoard work, increase latency, and make queue depth graphs lie.

The useful way to think about prefetch is unfinished work. A prefetch of 20 means a consumer can have 20 messages delivered but not yet acknowledged. Those messages are no longer ready in the queue. They are unacked, sitting with the consumer until it acks, nacks, rejects, or disconnects.

That means prefetch is not just a throughput knob. It is a fairness knob, a memory knob, and a failure-recovery knob.

What basic.qos does in RabbitMQ

Consumers set prefetch with basic.qos. In most client libraries you set prefetch_count; prefetch_size is rarely used and is usually left at zero.

In Python with Pika:

channel.basic_qos(prefetch_count=10)
channel.basic_consume(
    queue="jobs",
    on_message_callback=handle_message,
    auto_ack=False,
)

In Node.js with amqplib:

await channel.prefetch(10);
await channel.consume("jobs", async (msg) => {
  try {
    await handleMessage(msg.content);
    channel.ack(msg);
  } catch (err) {
    channel.nack(msg, false, false);
  }
}, { noAck: false });

Manual acknowledgement matters. If you use automatic acknowledgements, RabbitMQ considers the message complete as soon as it is delivered. Prefetch no longer protects processing reliability in the same way, because there is no unacknowledged window to manage.

RabbitMQ applies prefetch per consumer by default in modern usage, even though AMQP's original wording is channel-oriented. Some clients expose a global flag. Be careful with it. A shared channel or connection-wide limit can create confusing interactions between consumers. Most services are easier to reason about when each consumer has its own channel and its own prefetch count.

Why prefetch changes latency

Imagine a queue with two consumers. Consumer A gets a batch of 100 messages and then hits a slow external API. Consumer B is healthy and fast, but those 100 messages are already assigned to A. RabbitMQ will not give them to B unless A rejects them or its channel closes.

From the queue's point of view, those messages are not ready. From the user's point of view, they are delayed. This is why a high prefetch can make a system look better in broker graphs while making real latency worse.

Low prefetch gives RabbitMQ more chances to distribute work fairly. High prefetch gives consumers more local work and fewer broker round trips. Neither is always correct.

Starting values that make sense

For slow jobs, start small. If each message calls a third-party API, writes several database rows, or does CPU-heavy transformation, try prefetch_count=1 to 10. You want a failed or slow consumer to hold only a small amount of work.

For medium jobs that take tens or hundreds of milliseconds and run on stable workers, values like 10, 20, or 50 are common starting points. Measure before going higher.

For very fast handlers where the broker and consumer are on a low-latency network, a higher prefetch can reduce round trips and improve throughput. Even then, avoid choosing a huge number just because it made a benchmark look good for five minutes. Watch consumer memory and tail latency.

A simple rule of thumb is to size prefetch around the amount of work a consumer can comfortably hold for a short window. If a worker processes about 20 messages per second and you are comfortable with roughly one second of local buffered work, a prefetch near 20 is a reasonable experiment.

How to tell if prefetch is too high

Prefetch is probably too high when:

  • messages_unacknowledged is large compared with active consumers.
  • Some consumers have many unacked messages while others are idle.
  • Message latency is high even when messages_ready is low.
  • Consumer memory rises during bursts.
  • A consumer crash causes a large wave of redeliveries.

That last point is easy to miss. If a worker holds 1,000 unacked messages and crashes, RabbitMQ can redeliver those messages. That is correct behavior, but it can create duplicate pressure on downstream systems if the handler is not idempotent.

Lowering prefetch often improves fairness and recovery behavior. It may reduce peak throughput a little, but it can improve the latency users actually feel.

How to tell if prefetch is too low

Prefetch is probably too low when:

  • Consumers have low CPU and low memory use while messages_ready keeps growing.
  • Processing time is very short, but delivery rate is limited.
  • Network latency between consumers and RabbitMQ is noticeable.
  • Increasing prefetch improves throughput without increasing tail latency or memory pressure.

The classic example is a fast worker that does a small in-memory calculation and acks immediately. With prefetch_count=1, it may spend too much time waiting for the next message. Raising prefetch gives it a small local buffer and keeps it busy.

Do not hide downstream bottlenecks

Prefetch tuning will not fix a slow database. It can only change how work is distributed and buffered. If every message waits on the same overloaded API, a higher prefetch may make throughput look better briefly while increasing timeouts and retries.

Measure inside the consumer. Log or emit metrics for time spent decoding the message, waiting on the database, calling external services, and acking. RabbitMQ can show you ready and unacked counts, but it cannot tell you why your handler takes eight seconds.

When a downstream service is rate-limited, prefetch should often be lower, not higher. Let the queue absorb the backlog visibly instead of hiding thousands of in-flight calls inside workers.

Prefetch and concurrency are different

A prefetch of 50 does not automatically mean your consumer processes 50 messages in parallel. It only means RabbitMQ may deliver 50 messages before receiving acknowledgements. Whether they run concurrently depends on your consumer code.

A single-threaded consumer with prefetch 50 may process one message at a time while 49 wait in memory. A worker pool with concurrency 10 and prefetch 50 may keep ten tasks active and forty buffered. Sometimes that buffer is useful. Sometimes it is just latency.

Match prefetch to actual concurrency. If your process can execute five handlers at once, a prefetch of 5 to 20 is easier to reason about than 500.

Ordering and fairness tradeoffs

RabbitMQ queues preserve order at the queue level, but consumer behavior can change the order in which work completes. With multiple consumers and prefetch greater than 1, message 20 may finish before message 3 because it went to a faster worker or had easier work.

For most work queues, completion order does not matter. For account updates, inventory changes, or workflows that must be processed in sequence, it might matter a lot. In those cases, using one queue per ordering key, sharding by key, or keeping prefetch low may be safer than chasing maximum throughput.

Fairness has a similar tradeoff. A low prefetch lets RabbitMQ hand out work more evenly because consumers come back for messages more often. A high prefetch rewards the consumers that receive messages first. If messages have uneven processing times, that can lead to one worker holding a pile of slow jobs while another worker finishes its batch quickly.

When people say "RabbitMQ load balancing is uneven," prefetch is one of the first things to check. The broker can only balance messages that have not already been delivered.

Failure behavior matters

Prefetch changes what happens when a consumer dies. With prefetch_count=1, one unacked delivery comes back when the channel closes. With prefetch_count=500, hundreds may come back at once. If the consumer performed partial side effects before crashing, those redeliveries can trigger duplicate writes, duplicate emails, or duplicate API calls unless the handler is idempotent.

That does not mean high prefetch is wrong. It means high prefetch belongs with idempotent handlers, clear retry rules, and monitoring for redelivery rates. If duplicate processing would be dangerous, keep the unacked window small until the application is built to handle it.

Look at the redelivered flag in the consumer. It is not a complete retry counter, but it is a useful signal that the message has been delivered before. For robust retry limits, track attempts in headers or in application state and route exhausted messages to a dead-letter queue.

Multiple queues and mixed workloads

One prefetch value rarely fits every queue. A service that consumes thumbnail.generate and email.send may need different settings for each. Thumbnail generation may be CPU-heavy and best with low concurrency. Email sending may be network-bound and tolerate more in-flight messages.

If a single process consumes several queues on one channel, QoS behavior can become harder to reason about. Prefer separate channels for meaningfully different workloads. That makes prefetch, monitoring, and failure handling more obvious.

Mixed message sizes are another warning sign. If a queue contains both tiny events and huge payloads, a count-based prefetch does not reflect memory pressure well. Ten small messages and ten large messages are not the same cost. In that situation, split the workload or move large payloads out of RabbitMQ and pass references instead.

Watch unacked per consumer, not only per queue

A queue-level unacked count tells you there is unfinished work, but it may hide skew. One consumer may hold most of the unacked messages while the rest are nearly empty. That often points to high prefetch, uneven message cost, or one unhealthy worker.

Use consumer-level metrics from the management UI, Prometheus, or rabbitmqctl list_consumers during a test. If the distribution is uneven, lowering prefetch or splitting slow message types can improve real latency even when total throughput changes only a little.

Revisit prefetch after deployments

Prefetch values age. A value that worked when a handler only wrote one database row may be wrong after the next release adds an API call, extra validation, or a larger payload. Treat prefetch as part of performance configuration, not a number you set once and forget.

After a consumer release, compare processing latency, unacked counts, redeliveries, and consumer memory with the previous version. If latency rises but CPU is not saturated, the handler may be waiting on something external and a lower prefetch may keep the system fairer. If CPU is high and each message is CPU-bound, adding workers or reducing per-message work may matter more than changing prefetch.

Document the reason for the chosen value near the consumer configuration. Future maintainers should know whether prefetch_count=5 was chosen for fairness, memory, ordering, downstream rate limits, or just as a temporary default.

Test with real message shapes

Do not tune prefetch with tiny fake messages if production messages are large JSON payloads or include expensive database lookups. Message size and handler cost matter.

A useful test loop is:

  1. Pick a prefetch value.
  2. Run a realistic publish rate for long enough to see steady behavior.
  3. Watch messages_ready, messages_unacknowledged, consumer CPU, consumer memory, processing latency, and error rate.
  4. Kill one consumer and see how many messages redeliver.
  5. Increase or decrease prefetch and repeat.

The best value is rarely the one with the highest short benchmark throughput. It is the value that keeps consumers busy, keeps latency acceptable, and fails in a way your system can handle.

A practical default

If you have no data yet, start with manual acknowledgements and prefetch_count=10 for ordinary work queues. Use 1 for slow, expensive, or strictly fair processing. Try 20 or 50 for fast, stable handlers after measuring. Go higher only when metrics show that delivery round trips are the bottleneck and consumers have memory headroom.

RabbitMQ prefetch tuning is not a one-time setup. Revisit it when message size changes, consumer code changes, downstream dependencies change, or you add more worker instances. The right prefetch value is the one that matches the current shape of the work.