Streams: replayable message logs

RabbitMQ · Advanced · 14 min

Some workloads need fan-out to many readers, replay and huge backlogs, which ordinary queues handle poorly.

A stream (x-queue-type: stream) is an append-only, replicated log, similar in spirit to a Kafka topic partition. Unlike a queue, consuming does not remove messages. Each consumer chooses where to start reading with x-stream-offset: first, last, next, a numeric offset or a timestamp, and can re-read the history at will.

That makes streams the right tool for:

  • Large fan-out: many consumers reading the same messages, without a queue per consumer.
  • Replay: rebuilding a cache or a read model from history.
  • Huge backlogs: millions of messages stored efficiently on disk.

Data is removed only by retention: x-max-length-bytes and x-max-age (such as 7D), applied by deleting whole segment files.

Streams can be used from AMQP 0-9-1 clients like pika, with some rules: the consumer must set a prefetch (basic_qos) and use manual acks (acks just grant credit; nothing is deleted). For the best throughput use the native stream protocol (port 5552) with the dedicated client libraries, which also support server-side offset tracking and super streams, partitioned streams for scaling out with ordering per partition.

Streams don't support dead-lettering, TTL per message or priorities. Use quorum queues for task processing and streams for event logs.

Example

import pika
ch = pika.BlockingConnection(pika.ConnectionParameters("localhost")).channel()
ch.queue_declare("events", durable=True, arguments={
    "x-queue-type": "stream",
    "x-max-age": "7D",                    # retention by age
    "x-max-length-bytes": 20_000_000_000, # and by size
})
for i in range(3):
    ch.basic_publish("", "events", f"event {i}".encode())

ch.basic_qos(prefetch_count=100)          # required for stream consumers
seen = []
def on_msg(c, method, props, body):
    seen.append(body.decode()); c.basic_ack(method.delivery_tag)
    if len(seen) == 3: c.stop_consuming()

ch.basic_consume("events", on_msg, arguments={"x-stream-offset": "first"})
ch.start_consuming()
print(seen)    # ['event 0', 'event 1', 'event 2']  -- still in the stream

Remember

  • Streams are append-only logs; reading doesn't remove messages
  • Consumers pick a start with x-stream-offset and can replay
  • Retention by age or size; native protocol and super streams for scale

Watch out for

  • Using a stream as a task queue and expecting acked messages to disappear.

Check yourself

  1. Ten services each need every message, and new services must read past history. Best fit?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.rabbitmq.com/docs/streams