0%
RabbitMQ Deep Dive: AMQP, Queues, and Delivery Guarantees
The Broker Model
Queues and Storage
Delivery Guarantees
Patterns
Flow Control and Memory Alarms
Prefetch is the credit mechanism between the broker and a consumer, and the broker runs a second, entirely separate credit mechanism between its own internal processes, which is the one that actually slows a publisher down before any alarm fires.
The chain of processes a publish traverses
Four hops, each a separate Erlang process with its own mailbox.
A published message passes through the connection's reader process, which parses frames off the socket, then the channel process, which applies routing, then each queue process the routing selected, and finally the queue's own storage. These are independent processes scheduled by the Erlang runtime, communicating by asynchronous message passing, and each has an unbounded mailbox.
Unbounded mailboxes are the hazard. If the reader can parse faster than the channel can route, the channel's mailbox grows without limit and the broker's memory use grows with it, invisibly, because the messages are not in any queue yet and no queue metric describes them. The same applies between the channel and a slow queue process.
Erlang's own scheduling does not help here. A process with a large mailbox is not descheduled, so the fast producer keeps producing and the slow consumer falls further behind. Credit flow exists specifically to put a bound on this, and it operates between every adjacent pair in the chain rather than only at the edges.
How credit_flow works
A grant-and-replenish loop with two numbers.
Each sender holds a credit balance for each receiver it sends to. Sending one message decrements the balance by one; when the balance reaches zero the sender blocks itself and stops reading its own input. The receiver, having processed a configured number of messages, sends a replenishment back, the sender's balance is restored, and it unblocks.
The two numbers are set by credit_flow_default_credit, whose default is {400, 200}: an initial balance of 400 and a replenishment of 200 after every 200 messages processed. Raising them raises throughput under burst and raises peak memory, because the in-flight count between every pair of processes in the chain scales with the initial credit.
The blocking is transitive, and that is the whole point. A slow queue process stops replenishing its channel, the channel exhausts its credit and stops reading from the reader, the reader exhausts its credit and stops reading the socket, and TCP backpressure reaches the publisher. The chain converts a slow queue into a slow socket without any component needing to know about the others.
The two credit mechanisms, side by side
Because the broker runs two and they are routinely confused.
Prefetch is credit from a queue to a consumer, held by the queue, replenished by an acknowledgement, configured per consumer or per channel through basic.qos, and observable as messages_unacknowledged. Internal credit flow is credit between adjacent Erlang processes, held by the sender, replenished by the receiver's progress, configured globally through credit_flow_default_credit, and observable as the flow state.
They govern opposite directions and cannot substitute for each other. A consumer with a prefetch of 1 applies no backpressure to publishers at all; a connection in flow says nothing about whether consumers are keeping up. The only thing they share is the pattern, which is that the broker's preferred way to bound anything is a credit balance replenished by evidence of progress rather than a rate limit.
The practical reason to hold them apart is diagnosis. A publisher complaining of slow publishing and a consumer complaining of slow delivery are different investigations with different settings and different metrics, and the word "flow control" covers both, which is why an incident involving one often has someone changing the other.
Tuning the credit, and when that is justified
One setting, applied globally, with a cost paid in memory.
credit_flow_default_credit is set in advanced.config as an Erlang term and takes effect for every process pair on the node, which is the first thing to understand about tuning it: there is no per-connection, per-queue or per-exchange form. Raising it to {800, 400} doubles the in-flight allowance everywhere, including between processes that were never the constraint.
The case where raising it helps is a bursty publisher against queues that are fast on average but occasionally slow, where the extra in-flight allowance absorbs the burst instead of propagating backpressure to the socket. The burst is then held in process mailboxes, which is memory the broker reports but no queue metric attributes, so raising the credit converts visible backpressure into invisible memory use.
The case where it does not help is sustained overload, which is the common case. If the publish rate exceeds what the queues can absorb, a larger credit delays the inevitable by exactly the extra allowance and then produces the same flow state with more memory consumed. Worse, the memory consumed may be enough to reach the watermark, at which point a node that was correctly rate-limiting one connection blocks every publisher on the node instead.
So the tuning rule is narrow: raise it only with a measured burst profile, a known memory headroom and the understanding that the setting is global. Lowering it is almost never useful, because the effect is to apply backpressure sooner, and the default already applies it at 400 messages between any pair of processes, which is a small enough number that lowering it mostly costs throughput.
Why this is not a rate limit
A distinction that matters when choosing where to put a limit.
Credit flow has no notion of time. It bounds the number of messages in flight between two processes, not the number per second, so it cannot be used to enforce a publishing rate and it does not produce a stable throughput figure. A broker with fast queues delivers hundreds of thousands of messages a second through the same credit mechanism that limits a broker with slow queues to a few thousand.
That makes it self-tuning in a way a rate limit is not. The achieved rate is whatever the slowest process in the chain can sustain, discovered continuously, with no configured number to go stale when the hardware or the workload changes. A configured rate limit of 10,000 a second becomes wrong the moment the disk is upgraded; credit flow does not.
The limitation is the other side of the same property: because it has no target rate, it cannot be used to protect anything other than the broker's own memory. A downstream database that cannot take more than 500 writes a second gets no help from credit flow, because the broker will happily accept messages faster than that and let the queue grow. Rate limiting for a downstream's benefit belongs at the publisher or in the consumer's own pacing, and the broker has no mechanism for it at all.
The flow state, and what it means
A connection state that is routinely misread as a problem.
A connection whose reader has exhausted its credit is reported in state flow, visible in the management UI and in rabbitmqctl list_connections state. The same state appears per channel. It means the connection is being rate-limited by the broker's internal credit, not that anything has failed.
Brief, intermittent flow under load is normal and is the mechanism working. Sustained flow is a diagnosis: something downstream of this connection cannot keep up, and the candidates are a queue process saturated by its own work, a queue with an expensive binding set, a quorum queue waiting on its Raft group, or simply a publish rate above what the queues can absorb.
What flow is not is a memory alarm. The connection is still open, publishing is still progressing, confirms are still arriving, and no connection.blocked has been sent. Teams see flow and reach for the memory watermark, which is unrelated; the correct reading is that the broker is applying backpressure correctly and the publisher is faster than the queues.
What credit flow does not cover
Three gaps, and they are where the memory goes.
Credit flow bounds the message-passing chain and nothing else. It does not bound a queue's own depth: once a message has been accepted into a queue, it sits there and the only limits are max-length, max-length-bytes and TTL. A queue growing to ten million messages is not a credit-flow situation and the flow state will be clean throughout.
It does not bound the consumer-side path either. Deliveries to consumers are governed by prefetch, which is the other credit mechanism, and the two are independent: a connection can be in flow for publishing while its consumers are idle. And it does not bound connection or channel counts, so ten thousand connections each with a modest reader buffer is a memory problem credit flow has no opinion about.
The result is a division of labour worth being explicit about. Credit flow bounds the rate, queue limits bound the depth, and the memory alarm is the backstop for everything neither of them covers. All three exist because each catches a different failure, and a deployment relying on only one of them has two unbounded directions.
A connection in state flow is not an error and is unrelated to the memory watermark. It means the broker's internal credit has been exhausted somewhere downstream and the reader has stopped reading the socket, which is backpressure working correctly. Brief flow under load is normal; sustained flow means a queue process cannot keep up with the publish rate, and the investigation belongs on the queue rather than on memory.
Loading problem...