On RabbitMQ 4.3.6, a stream read over its own protocol moved 281,330 messages per second in my test, 6.2 times more than a quorum queue, which topped out at 45,156. Both are replicated queues in the same broker, but their contracts differ: the quorum queue deletes each message once the consumer acknowledges it and fsyncs before confirming to the publisher, and the stream keeps everything so anyone can read it again. I measured both on 16 September 2026 with PerfTest and Stream PerfTest, the official tools, on a 4-core node with 1 KB messages. This comparison is also available in Spanish.

Key takeaways

  • With one producer, one consumer and 1 KB messages, the quorum queue sustained 45,156 msg/s and the stream, over the stream protocol, 281,330 msg/s (medians of three runs).
  • A stream read over AMQP 0-9-1 accepted 120,860 msg/s, but the consumer only drained 62,360 msg/s and end-to-end latency reached 9.1 s.
  • At a fixed 10,000 msg/s, the quorum queue had a median latency of 2.73 ms and a p99 of 33.6 ms; the stream over AMQP, 0.80 ms and 5.39 ms.
  • One million messages took 1.24 GB of disk in the quorum queue and 1.11 GB in the stream, but the quorum queue process used 20.5 MB of memory and the stream process 9 KB.
  • With one unacknowledged message, RabbitMQ 4.2.9 kept 1.02 GB of quorum queue segments and 4.3.6 left less than 9 MB.
  • The stream does not fsync before confirming and has no dead lettering, TTL or priorities; the quorum queue has all of them, and 4.3 added delayed retry and consumer timeouts.

What is the difference between a stream and a quorum queue

A quorum queue is a FIFO queue replicated with the Raft consensus algorithm: it delivers each message to one consumer and deletes it once that consumer acknowledges it. A stream is an append-only log: reading deletes nothing, and each consumer chooses the position (offset) it starts from. The RabbitMQ streams guide[1] puts it like this: "streams were not introduced to replace queues but to complement them".

You declare both types with the x-queue-type argument (quorum or stream), and no policy can change that type afterwards. The difference that weighs most in production is durability. According to the same guide, streams do not force a flush to disk (fsync) and leave that to the operating system. A quorum queue, by contrast, does not confirm a message until a majority of its replicas have written and flushed it.

The quorum queue documentation[2] presents them as the default choice for a replicated, highly available queue. It also says when not to use them: temporary queues, lowest possible latency, backlogs above 5 million messages and large fan-outs. For those last two cases it recommends a stream.

What changed in RabbitMQ 4.3 for quorum queues and streams

The 4.3 series was published on 23 April 2026, and its 4.3.6 patch, the one I tested, on 14 September. The RabbitMQ release information page[3] puts the end of community support for the series on 30 November 2026. According to the RabbitMQ 4.3.0 release notes[4], these are the changes that affect this comparison:

  • Metadata: Khepri becomes the only store, so a cluster needs a majority of its nodes online to be available.
  • Quorum queues: a new state machine version with strict priorities across 32 levels, delayed retry (x-delayed-retry-type) and consumer timeouts.
  • Quorum queue disk: log compaction reclaims space even when messages remain unacknowledged or are acknowledged out of order.
  • Streams: they no longer evaluate consumer timeouts, and the stream.read_ahead setting controls how much data is read from disk ahead of time.
  • Classic queues: transient non-exclusive queues are denied by default.

Later patches touch streams twice. Version 4.3.2 fixed a performance regression in stream protocol frame assembly. Version 4.3.5 rejects more than 256 publishers or 256 subscriptions per connection up front, a limit that comes from the protocol’s wire format. The 4.3.6 release notes[5] fix another stream detail: x-max-age is now compared as a duration, so redeclaring a stream with 1D when it was created with 24h no longer fails.

I checked classic queues on 4.3.6. Declaring a transient non-exclusive queue through the HTTP API returned a 400 with the message Feature transient_nonexcl_queues is deprecated. A queue with x-queue-mode=lazy, however, was created without an error over both AMQP and HTTP, even though the 4.3.0 notes say that argument makes the declaration fail.

How I ran the test

The test ran on a single RabbitMQ 4.3.6 node with Erlang 27.3.4.17, inside Docker and limited to 4 CPUs and 4 GiB of memory. The machine is an 18-core arm64 Linux VM with 121 GB of RAM (OrbStack on Apple silicon), shared with other workloads. The Docker volume sits on btrfs over a virtio disk. This is the compose.yaml, with shortened names:

services:
  rabbit:
    image: rabbitmq:4.3.6-management
    hostname: bench-rabbit
    cpus: 4
    mem_limit: 4g
    ports:
      - "127.0.0.1:15672:15672"
    volumes:
      - ./enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
      - ./20-bench.conf:/etc/rabbitmq/conf.d/20-bench.conf:ro
      - rabbit-data:/var/lib/rabbitmq
    networks: [bench-net]
volumes:
  rabbit-data:
networks:
  bench-net:

The enabled_plugins file turns on the management UI, Prometheus and both stream plugins, which open port 5552. The 20-bench.conf file sets the memory threshold to an absolute value:

# enabled_plugins
[rabbitmq_management,rabbitmq_prometheus,rabbitmq_stream,rabbitmq_stream_management].

# 20-bench.conf
vm_memory_high_watermark.absolute = 2400MiB

That second file is not optional. Without it, RabbitMQ computed its threshold as 78.4 GB, 60 % of the host’s RAM, even though the container had a 4 GiB limit. The RabbitMQ memory guide[6] recommends an absolute threshold in containers because the node does not always detect the cgroups limit.

Each scenario uses one producer, one consumer, 1000-byte messages and publisher confirms, and each tool runs in its own container with 4 CPUs. For the quorum queue and for the stream over AMQP 0-9-1 I used PerfTest[7] 2.25.0, released on 30 June 2026:

docker run --rm --network bench-net --cpus 4 \
  pivotalrabbitmq/perf-test:2.25.0 \
  --uri amqp://guest:guest@bench-rabbit \
  -x 1 -y 1 -s 1000 -c 1000 -q 1000 \
  --quorum-queue --queue qq-1 -z 30

For the stream over AMQP I swapped --quorum-queue for --stream-queue. For the stream protocol I used Stream PerfTest[8] 1.8.0, released on 9 July 2026, with its defaults (batches of 100 messages and up to 10,000 pending confirms):

docker run --rm --network bench-net --cpus 4 \
  pivotalrabbitmq/stream-perf-test:1.8.0 \
  --uris rabbitmq-stream://guest:guest@bench-rabbit:5552 \
  -x 1 -y 1 -s 1000 -z 30 --streams sp-1 -cl -ds

The latency tests add --rate 10000 to the same commands. Each run lasts 30 s, and I report the median of three valid runs per scenario.

Before each one I waited for the one-minute load average to drop below 6 on the 18 cores. I discarded 2 of the 20 runs because they overlapped with another container’s load. Across the 18 valid runs, load ranged from 1.94 at the start to 5.80 at its peak during a run.

How many messages per second each one moves

Without a rate limit, the stream protocol moved 6.2 times the messages of the quorum queue. The stream read over AMQP 0-9-1 landed in between, with a problem of its own:

Scenario Published/s Consumed/s Median latency p99 latency
Quorum queue, AMQP 0-9-1 45,164 45,156 10.8 ms 35.0 ms
Stream, AMQP 0-9-1 120,860 62,360 9.1 s 15.1 s
Stream, stream protocol 281,781 281,330 24 ms 40 ms

Latency is end to end, from the moment the producer publishes until the consumer receives. In the stream over AMQP, the consumer drained half of what came in, so the delay grew for the whole run until it reached a 9.1 s median. The publisher did go 2.7 times faster than with the quorum queue. Its confirm, which does not wait for an fsync, took a 5.9 ms median against 10.2 ms.

The introduction to streams on the RabbitMQ blog[9], from 2021, claimed that streams beat traditional queues by a factor of 100 or more. On this node, with 1 KB messages, the difference was 6.2 times, below even a factor of 10. The stream protocol comparison table[10] talks about "Hundreds of thousands per second" over AMQP and "Millions messages per second" with the plugin; here the plugin stopped at 281,330.

The ceiling for the stream protocol came from the container’s memory limit. In three of the four runs there were stretches of about 5 s at 500,000 to 600,000 msg/s. Between them, the publisher stalled for 3 to 4 s.

About 12 s after publishing began, the container hit its 4 GiB, and 3.8 GB of that was kernel page cache. Streams write through that cache, which counts as used memory inside a container. Everything that was not cache added up to less than 470 MB.

To check this I repeated only that scenario with the container limited to 16 GiB. The three runs, at a load of 1.82 to 4.30, gave a median of 517,474 msg/s published and 517,072 consumed. There were no pauses: no second dropped below 376,000 msg/s, and the cache grew to 16 GB. If your broker will move streams at that rate, size the container’s memory with the cache in mind, not just RabbitMQ’s threshold.

How much latency each one adds at 10,000 messages per second

All three setups sustain a fixed rate of 10,000 msg/s. At that rate, the quorum queue added 3.4 times more median latency than the stream over AMQP, and 6.2 times more at p99:

Scenario End to end, median End to end, p99 Confirm, median Confirm, p99
Quorum queue, AMQP 0-9-1 2.73 ms 33.6 ms 2.58 ms 33.2 ms
Stream, AMQP 0-9-1 0.80 ms 5.39 ms 0.44 ms 1.57 ms
Stream, stream protocol 1 ms 2 ms under 1 ms 2 ms

In the quorum queue, confirm latency and end-to-end latency almost match: the message reaches the consumer as soon as the write completes, and the write includes the fsync. The p99 spikes, between 22.8 and 54.2 ms depending on the run, fit the cost of that flush on a virtual disk, although I did not isolate it. Stream PerfTest rounds latency to whole milliseconds, so its row only says that the stream protocol stayed at 1 or 2 ms with batches of 100 messages.

How much disk and memory one million messages take

One million 1000-byte messages took a similar amount of disk in both types, but memory told another story. I published the million with no consumers using PerfTest and measured the data directory with du, each queue’s memory with rabbitmqctl list_queues and the node breakdown with rabbitmq-diagnostics memory_breakdown:

With 1,000,000 messages of 1000 bytes Quorum queue Stream
Disk after publishing 1.24 GB 1.11 GB
Queue process memory 20.5 MB 9 KB
In-memory log tables (quorum_ets) 130 MB Not applicable
Disk after consuming everything 272 MB, only the WAL 1.11 GB, untouched
Messages for a second consumer 0 1,000,000

The quorum queue documentation estimates at least 32 bytes of in-memory metadata per message, which would mean 32 MB or more for a million. I measured 20.5 MB, about 20.5 bytes per message, in line with the compact message references that, according to the 4.3.0 notes, can halve per-message memory overhead. The management UI shows the same ratio with half a million messages: 9.7 MiB of process memory. The same page shows the default priority 4 the queue assigns and the delivery limit of 20.

Details of the pedidos quorum queue in RabbitMQ 4.3.6: 500,000 messages at priority 4, a delivery limit of 20, delayed retry not enabled and 9.7 MiB of process memory.

The stream, holding 500,001 messages, stays at 8.9 KiB of process memory. According to its guide, it stores everything on disk and keeps in memory only what it has not written yet. The page also shows the segment count and the timestamp of the oldest message, a field the UI added in 4.3.2:

Details of the eventos stream in RabbitMQ 4.3.6: 500,001 messages in 2 segments of up to 500 MB, the oldest message timestamp and 8.9 KiB of process memory.

One unacknowledged message no longer pins the disk

The new compaction in 4.3 shows when a consumer holds on to one unacknowledged message. I tested it on two clean nodes, one on 4.3.6 and one on 4.2.9. A PerfTest consumer with prefetch 1 and 900 s of simulated latency (-q 1 -L 900000000) holds the first message. Another consumer drains the remaining 999,999, and I measure the queue directory over the next 4 minutes:

With one unacknowledged message RabbitMQ 4.2.9 RabbitMQ 4.3.6
Segments after draining the queue 194 files, 1.02 GB 8.9 MB outside the WAL
Whole directory, WAL included 1.35 GB 398 MB
Change after 4 minutes None None

On 4.2.9, the 194 segments stayed on disk even after I disconnected the holding consumer. The message went back to the queue, and that version does not truncate the log past the oldest live message. On 4.3.6, the first measurement after draining the queue already showed 8.9 MB outside the WAL. The WAL, shared by all quorum queues on the node, is not reclaimed this way: it is recycled when it reaches its maximum size, 512 MiB by default.

Rereading messages: what a quorum queue cannot do

A stream keeps messages after they are read, and the test’s million stayed available across nine full reads. A new consumer starts from the beginning with the x-stream-offset argument set to first, which PerfTest exposes like this:

docker run --rm --network bench-net pivotalrabbitmq/perf-test:2.25.0 \
  --uri amqp://guest:guest@bench-rabbit -x 0 -y 1 \
  --stream-queue --queue eventos -sco first -D 1000000

Across three reads over AMQP 0-9-1, the median was 69,773 msg/s, about 15 s per read including container startup. With Stream PerfTest and --offset first, the median of another three was 464,683 msg/s, 3.3 s per read including the JVM. Load was between 0.98 and 2.93. Every read ended when it received the million, and after the first three the stream directory was still at 1.11 GB.

In the quorum queue, by contrast, a second consumer connected 10 s after draining it received no messages at all. In the stream, rabbitmqctl counted 1,000,002 messages for 1,000,000 published; the streams guide warns that this counter can slightly exceed the real one.

What each queue type offers in RabbitMQ 4.3

The features of each type, according to the 4.3 documentation, separate the use cases more clearly than the numbers do:

Feature Quorum queue Stream
Reading Destructive: the ack deletes Non-destructive, by offset
Rereading messages No From the start, an offset or a timestamp
fsync before confirming Yes No, the operating system decides
Dead lettering Yes, at-least-once too No
TTL and length limit Yes No, retention by size or age
Priorities Strict, 32 levels No
Delayed retry and timeout Yes, since 4.3 No
Delivery limit (poison messages) Yes No
Publish deduplication No Yes, with the stream protocol
Replica changes Semi-automatic Manual, with rabbitmq-streams

Stream retention is evaluated per segment, 500 MB by default, and at least one segment with messages always remains. A stream without x-max-length-bytes or x-max-age deletes nothing and grows until the disk is full, so set retention when you declare it.

When to pick a stream and when to pick a quorum queue

Pick the quorum queue for work that is handed out and acknowledged once, and the stream when two or more readers need the same messages or when you will read them again. In practice:

  • Quorum queue: orders, payments and tasks with retries, where a failing message must return to the queue or end up in a dead letter queue.
  • Stream: auditing, event reprocessing, large fan-outs and backlogs of millions of messages.
  • Stream read over AMQP 0-9-1: fine for slow readers or for starting out without switching libraries, but in this test the consumer did not get past 62,360 msg/s; for higher rates, use a stream protocol client.
  • Kafka or Redpanda: when you need managed partitions, connectors and a streaming ecosystem; see Kafka without ZooKeeper in production and the Redpanda comparison with Kafka.

If you are still deciding whether RabbitMQ is the right broker, start with when RabbitMQ is still the choice for message queues, and for the overall design, with event-driven architecture.

Frequently asked questions

Can I turn a quorum queue into a stream with a policy?

No. The type is set with x-queue-type when the queue is declared, and no policy changes it. To move from one to the other you create the stream, move the producers over and drain the old queue.

Do I need the stream plugin to use a stream?

No. Any AMQP 0-9-1 client that supports queue and consumer arguments can declare and read a stream, with a prefetch set. The plugin adds port 5552, deduplication, super streams, single active consumer and server-side offset tracking.

Does a RabbitMQ stream replace Kafka?

For a fan-out or an event log inside the broker you already run, it can be enough. Kafka still has more tooling around the log, such as connectors and stream processing, and RabbitMQ recommends super streams only when a single stream falls short.

Conclusion

On RabbitMQ 4.3.6, the quorum queue and the stream solve different jobs. The quorum queue moved 45,156 msg/s with an fsync on every confirm, priorities, retries and dead lettering, and 4.3 finally frees its disk even when one message is stuck. The stream moved 281,330 msg/s over its protocol with 4 GiB of memory and 517,072 with 16 GiB. It also kept a million rereadable messages with 9 KB of process memory.

Before you decide, repeat these tests with your message size and on your disk. Here the stream’s ceiling came from the container’s memory limit, on a virtual disk. A three-node cluster also adds the cost of replication, which I did not measure. To watch queue depth and node memory afterwards, the rabbitmq_prometheus plugin in this setup works with Prometheus in Docker.

Sources

  1. RabbitMQ streams guide
  2. quorum queue documentation
  3. RabbitMQ release information page
  4. RabbitMQ 4.3.0 release notes
  5. 4.3.6 release notes
  6. RabbitMQ memory guide
  7. PerfTest
  8. Stream PerfTest
  9. introduction to streams on the RabbitMQ blog
  10. stream protocol comparison table
  11. PerfTest 2.25.0 release
  12. Stream PerfTest 1.8.0 release