0%
RabbitMQ Deep Dive: AMQP, Queues, and Delivery Guarantees
The Broker Model
Delivery Guarantees
Flow and Scale
Patterns
Streams, an Append-Only Log Inside RabbitMQ
A stream is the third queue type, and it is the one that is not a queue. It is an append-only log with retention, reachable through the same broker, the same vhosts and the same permissions as everything else, and almost everything surprising about it follows from reads being non-destructive.
Reading does not remove
The single property everything else depends on.
A classic or quorum queue holds messages until a consumer acknowledges them, at which point they are gone. A stream holds messages until retention removes them, and a consumer reading a message changes nothing about the stream's contents. Ten consumers can read the same message, independently, at different times and at different rates, and the stream does not know or care.
That inverts where position is held. A queue holds the position implicitly: the head is whatever has not been acknowledged yet, and the broker owns it. A stream's consumers each hold an offset, a 64-bit absolute position in the log, and reading is "give me messages from offset N onward". The broker can help store that offset, but the offset belongs to the consumer.
So the mental model that works is a file that is only ever appended to, with several processes reading it at their own pace and an operator deciding how much of the beginning to keep. Every capability and every limitation in this lesson is a consequence of that model.
Osiris, and the two different replication layers
A Raft group for coordination, something else for the data.
Streams are implemented by Osiris, a library separate from the ra Raft implementation that quorum queues use. Each stream has a leader that accepts writes and replicas that follow it, and the log is replicated from leader to replicas directly rather than through a consensus protocol per entry.
There is still Raft involved, and the distinction matters. A single cluster-wide stream coordinator is a Raft group that holds the metadata: which streams exist, who leads each one, where the replicas are. Membership changes and leader elections go through that coordinator. The message data does not, which is what lets a stream sustain throughput a per-entry consensus protocol could not.
The practical reading is that a stream's availability depends on the coordinator being healthy, and its write throughput depends on the leader and its replicas. These fail differently: a coordinator problem affects declaration and failover across every stream, while a replica problem affects one stream's durability margin.
Confirms, and what a stream guarantees
A quorum of replicas, written to disk.
Publishing to a stream uses the same basic.publish with publisher confirms as anything else, and the confirm is sent once the data has been written and replicated to a quorum of the stream's replicas. With the default of three members that is two, exactly as with a quorum queue, so the durability guarantee is comparable.
The difference is in what is being written. A quorum queue appends an entry per operation and must keep the state machine's view of the queue; a stream appends a chunk containing many messages and has no per-message state to maintain, because there is no acknowledgement to track and no delivery count to update. That is where the throughput comes from: the per-message work is close to the cost of appending bytes to a file.
A stream is always durable and always replicated. There is no transient stream, no non-durable declaration and no single-copy mode, so the floor cost is a replicated disk write, as with a quorum queue. The difference is that the floor is amortised across a chunk rather than paid per message.
Declaring one, and what cannot be changed afterwards
Two arguments, both fixed at declaration.
A stream is declared like any queue, with x-queue-type set to stream, and it must be durable and not exclusive or auto-delete. x-initial-cluster-size sets the number of members, defaulting to the cluster size up to three, and like a quorum queue's group size it is a declaration-time property.
x-max-segment-size-bytes sets the segment size for this stream and is also fixed at declaration. That one is easy to overlook and genuinely consequential, because segment size determines retention granularity, as the next two sections develop in detail.
Retention, by contrast, is changeable by policy: max-age and max-length-bytes can be set and adjusted on a live stream. The division is the same one that applies everywhere in RabbitMQ: structural properties are declaration arguments and take a new queue to change, while operational limits are policy and take effect immediately.
Replicas are not read replicas, and what that costs
One leader, every read.
A stream's replicas exist for durability and for failover, not for read scaling. Consumers connect through the leader, so a stream with three members is not serving reads three ways; it is serving them from one node with two nodes holding copies against that node's failure.
This is the same restriction quorum queues have, and the reason is similar: a follower's view can lag the leader's, so serving a read from it would mean serving a position that does not exist yet from the leader's perspective. Rather than expose that, the design keeps reads on the leader.
The practical consequence is that a single stream's read throughput has a single node's ceiling, and the lever for exceeding it is a super stream, covered later, rather than more replicas. Adding replicas to a stream under read pressure does nothing for the symptom and adds replication traffic, which is one of the more common wasted interventions.
Leader placement therefore matters. stream_leader_locator chooses where a new stream's leader lands, with balanced spreading leaders across nodes and client-local putting the leader where the declaring connection is, and a cluster where every stream was declared from the same client ends up with every leader on one node and a very uneven load.
What the broker does not track
Three absences, each of which becomes application code.
A stream does not track who has read what. There is no equivalent of an unacknowledged count, no per-consumer position the broker maintains unless the consumer is using the stream protocol's offset tracking, and no notion of a message having been fully consumed. A message's fate after being read is entirely outside the broker's knowledge.
A stream does not track delivery attempts. There is no delivery count, so there is nothing to compare against a limit and no mechanism for recognising a message that keeps failing. The whole poison-message apparatus that x-delivery-limit provides on a quorum queue has no counterpart.
A stream does not track consumer liveness against retention. It will delete a segment that a connected, healthy, merely slow consumer has not reached, and it will not warn anybody. The consumer discovers this by receiving older data than it expected, if it notices at all.
Each of those absences is deliberate, because tracking any of them per consumer would reintroduce the per-message state that makes a queue expensive. But each one becomes a responsibility somewhere, and that somewhere is the application and its monitoring. A stream is cheap partly because it does less, and the work it does not do has not disappeared.
Where a stream sits among the three types
A different question, not a better answer.
A classic queue answers "hold work for a consumer, on one node, as fast as possible". A quorum queue answers "hold work for a consumer and survive losing the node". A stream answers "keep a record and let several readers consume it at their own pace", which is not the same question at all.
The tell that a stream is the right structure is a requirement that a queue cannot express: replay from a point in the past, several independent consumers needing the same messages, retention measured in time rather than in backlog, or a volume where depth is normal rather than an incident. Any of those in a requirements document is a stream, however the requirement was originally phrased.
The opposite tell is per-message handling: a message that can fail and need retrying, a message that needs to go to a dead-letter queue, a message that must be processed exactly once by exactly one of several workers. Those are queue semantics, and a stream does not have them.
Everything distinctive about a stream follows from reads being non-destructive. Consumers hold 64-bit offsets rather than the broker holding a head, so ten readers can consume the same message independently, and only retention removes data. That is why a stream can replay and a queue cannot, and also why a stream has no dead-lettering, no selective acknowledgement and no redelivery: there is nothing to remove and nothing to put back.