KubeMQ
ConnectorsRabbitMQ (AMQP 0-9-1)How-to guides

Pub/Sub (Fanout)

Broadcast every message to all subscribers over AMQP 0-9-1 — a fanout exchange copies to exclusive queues, each backed by its own KubeMQ Queue channel.

Publish/subscribe broadcasts every message to all interested subscribers. In AMQP this is a fanout exchange: every queue bound to the exchange receives a copy, and the routing key is ignored. Each subscriber typically declares its own server-named, exclusive queue, so subscribers are independent and their queues vanish when they disconnect. Each of those queues is an ordinary KubeMQ Queue channel — the exchange itself is virtual connector-side routing resolved at publish time, not a data store.

Overview

The publisher declares a fanout exchange and publishes to it; the routing key is ignored. Each subscriber declares a server-named exclusive queue (queue.declare("") → an amq.gen-* name) and binds it to the exchange with an empty routing key. At publish time the connector resolves the fanout into the set of bound queues and writes a copy to each one's KubeMQ channel — so every subscriber gets its own copy.

OperationAMQP actionKubeMQ mapping
Declare exchangeexchange.declare("logs", "fanout")Virtual fanout routing entry (no storage)
Subscribequeue.declare("") + queue.bind(q, "logs", "")Server-named queue channel amqp.default.amq.gen-*
Publishbasic.publish(exchange="logs", routing-key="")Copy fanned out to every bound queue channel
Receivebasic.consume(q) (auto-ack)One copy per subscriber

How it works

A published message fans out to every queue bound to the exchange. Because each subscriber binds its own exclusive queue, every subscriber receives every message independently — there is no competition between them.

The virtual fanout exchange resolves to every bound queue; the connector writes a copy to each queue channel, so each subscriber receives its own copy.

Publish and subscribe

Each example declares a non-durable fanout exchange logs, attaches two subscribers (each with its own server-named exclusive queue bound with the empty key), then broadcasts a batch of messages on a confirm channel. Each subscriber independently receives every message. Every client reads the broker endpoint from KUBEMQ_AMQP_URL (default amqp://guest:guest@localhost:5672/).

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

const exchange = "logs"
const messages = 5

func amqpURL() string {
	if v := os.Getenv("KUBEMQ_AMQP_URL"); v != "" {
		return v
	}
	return "amqp://guest:guest@localhost:5672/"
}

func subscribe(conn *amqp.Connection) (string, <-chan amqp.Delivery) {
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("subscriber channel: %v", err)
	}
	if err := ch.ExchangeDeclare(exchange, "fanout", false, false, false, false, nil); err != nil {
		log.Fatalf("declare exchange: %v", err)
	}
	q, err := ch.QueueDeclare("", false, false, true, false, nil) // server-named, exclusive
	if err != nil {
		log.Fatalf("declare queue: %v", err)
	}
	if err := ch.QueueBind(q.Name, "", exchange, false, nil); err != nil { // key ignored
		log.Fatalf("bind: %v", err)
	}
	msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil) // auto-ack
	if err != nil {
		log.Fatalf("consume: %v", err)
	}
	return q.Name, msgs
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	conn, err := amqp.Dial(amqpURL())
	if err != nil {
		log.Fatalf("dial: %v", err)
	}
	defer func() { _ = conn.Close() }()

	nameA, subA := subscribe(conn)
	nameB, subB := subscribe(conn)
	log.Printf(" [s] bound exclusive queues %q and %q to fanout %q", nameA, nameB, exchange)

	// Publish on a confirm channel so every broadcast is accepted before we stop.
	pubCh, _ := conn.Channel()
	if err := pubCh.Confirm(false); err != nil {
		log.Fatalf("confirm select: %v", err)
	}
	confirms := pubCh.NotifyPublish(make(chan amqp.Confirmation, messages))
	for i := 0; i < messages; i++ {
		if err := pubCh.PublishWithContext(ctx, exchange, "", false, false, amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(fmt.Sprintf("log-%d", i)),
		}); err != nil {
			log.Fatalf("publish %d: %v", i, err)
		}
	}
	for i := 0; i < messages; i++ {
		<-confirms
	}
	log.Printf(" [x] Broadcast %d messages to fanout %q", messages, exchange)

	// Each subscriber independently receives all 5 copies.
	for label, msgs := range map[string]<-chan amqp.Delivery{"A": subA, "B": subB} {
		got := 0
		for got < messages {
			select {
			case <-msgs:
				got++
			case <-ctx.Done():
				log.Fatalf("subscriber %s timed out (%d/%d)", label, got, messages)
			}
		}
		log.Printf(" [s%s] received all %d broadcasts", label, got)
	}
}
import os

import pika

URL = os.environ.get("KUBEMQ_AMQP_URL", "amqp://guest:guest@localhost:5672/")
EXCHANGE = "logs"
SUBSCRIBERS = 2
MESSAGES = 5


def main() -> None:
    # Subscribers — each on its own connection with a server-named exclusive
    # queue bound to the fanout exchange.
    subs = []
    for _ in range(SUBSCRIBERS):
        conn = pika.BlockingConnection(pika.URLParameters(URL))
        ch = conn.channel()
        ch.exchange_declare(exchange=EXCHANGE, exchange_type="fanout", durable=False)
        queue = ch.queue_declare(queue="", exclusive=True).method.queue  # amq.gen-*
        ch.queue_bind(exchange=EXCHANGE, queue=queue, routing_key="")  # key ignored
        print(f" [sub] bound exclusive queue {queue!r}")
        subs.append((conn, ch, queue))

    # Publisher — broadcast 5 messages on a confirm channel.
    pub_conn = pika.BlockingConnection(pika.URLParameters(URL))
    pub_ch = pub_conn.channel()
    pub_ch.exchange_declare(exchange=EXCHANGE, exchange_type="fanout", durable=False)
    pub_ch.confirm_delivery()
    for i in range(MESSAGES):
        pub_ch.basic_publish(exchange=EXCHANGE, routing_key="", body=f"log-{i}".encode())
    print(f" [x] Broadcast {MESSAGES} messages to {EXCHANGE!r}")

    # Each subscriber independently receives every copy.
    for idx, (conn, ch, queue) in enumerate(subs):
        got = 0
        for method, _props, _body in ch.consume(queue, inactivity_timeout=30, auto_ack=True):
            if method is None:
                raise SystemExit(f"subscriber {idx}: timed out ({got}/{MESSAGES})")
            got += 1
            if got >= MESSAGES:
                break
        ch.cancel()
        print(f" [sub {idx}] received all {got} broadcasts")
        ch.close()
        conn.close()

    pub_ch.close()
    pub_conn.close()


if __name__ == "__main__":
    main()
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public final class Main {
    private static final String EXCHANGE = "logs";
    private static final int SUBSCRIBERS = 2;
    private static final int MESSAGES = 5;

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(System.getenv().getOrDefault("KUBEMQ_AMQP_URL", "amqp://guest:guest@localhost:5672/"));
        if (factory.getVirtualHost() == null || factory.getVirtualHost().isEmpty()) {
            factory.setVirtualHost("/"); // trailing "/" parses to an empty vhost
        }

        Connection connection = factory.newConnection();

        // Subscribers — each with a server-named exclusive queue bound to fanout.
        List<CountDownLatch> latches = new ArrayList<>();
        for (int i = 0; i < SUBSCRIBERS; i++) {
            Channel ch = connection.createChannel();
            ch.exchangeDeclare(EXCHANGE, "fanout", false);
            String queue = ch.queueDeclare("", false, true, true, null).getQueue(); // amq.gen-*
            ch.queueBind(queue, EXCHANGE, ""); // key ignored for fanout
            CountDownLatch latch = new CountDownLatch(MESSAGES);
            ch.basicConsume(queue, true, (tag, delivery) -> latch.countDown(), tag -> { });
            System.out.println("[sub] bound exclusive queue " + queue);
            latches.add(latch);
        }

        // Publisher — broadcast 5 messages on a confirm channel.
        Channel pub = connection.createChannel();
        pub.exchangeDeclare(EXCHANGE, "fanout", false);
        pub.confirmSelect();
        for (int i = 0; i < MESSAGES; i++) {
            pub.basicPublish(EXCHANGE, "", null, ("log-" + i).getBytes(StandardCharsets.UTF_8));
        }
        pub.waitForConfirmsOrDie(30_000);
        System.out.println("[x] Broadcast " + MESSAGES + " messages to fanout '" + EXCHANGE + "'");

        // Each subscriber independently receives all 5 copies.
        for (int i = 0; i < latches.size(); i++) {
            if (!latches.get(i).await(30, TimeUnit.SECONDS)) {
                throw new IllegalStateException("subscriber " + i + " did not receive all broadcasts");
            }
            System.out.println("[sub " + i + "] received all " + MESSAGES + " broadcasts");
        }
        connection.close();
    }
}
import amqp, { type Channel, type ChannelModel } from "amqplib";

const EXCHANGE = "logs";
const MESSAGES = ["log-0", "log-1", "log-2", "log-3", "log-4"];

function url(): string {
  return process.env["KUBEMQ_AMQP_URL"] ?? "amqp://guest:guest@localhost:5672/";
}

async function subscribe(connection: ChannelModel): Promise<{ name: string; done: Promise<void> }> {
  const ch: Channel = await connection.createChannel();
  await ch.assertExchange(EXCHANGE, "fanout", { durable: false });
  const q = await ch.assertQueue("", { exclusive: true }); // amq.gen-*
  await ch.bindQueue(q.queue, EXCHANGE, ""); // routing key ignored for fanout
  let received = 0;
  const done = new Promise<void>((resolve) => {
    ch.consume(q.queue, (msg) => {
      if (msg === null) return;
      if (++received === MESSAGES.length) resolve();
    }, { noAck: true });
  });
  return { name: q.queue, done };
}

async function main(): Promise<void> {
  const connection = await amqp.connect(url());

  const subA = await subscribe(connection);
  const subB = await subscribe(connection);
  console.log(`[sub] bound exclusive queues ${subA.name} and ${subB.name}`);

  // Publisher — broadcast on a confirm channel so no message is lost to an early close.
  const pubCh = await connection.createConfirmChannel();
  await pubCh.assertExchange(EXCHANGE, "fanout", { durable: false });
  for (const body of MESSAGES) pubCh.publish(EXCHANGE, "", Buffer.from(body));
  await pubCh.waitForConfirms();
  console.log(`[x] Broadcast ${MESSAGES.length} messages to fanout "${EXCHANGE}"`);

  await Promise.all([subA.done, subB.done]);
  console.log("both subscribers received all 5 broadcasts independently");
  await connection.close();
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

const string exchange = "logs";
const int subscribers = 2;
const int messages = 5;

static string Url() =>
    Environment.GetEnvironmentVariable("KUBEMQ_AMQP_URL") is { Length: > 0 } v
        ? v
        : "amqp://guest:guest@localhost:5672/";

var factory = new ConnectionFactory { Uri = new Uri(Url()) };
await using var connection = await factory.CreateConnectionAsync("pub-sub-fanout");

// Subscribers — each with a server-named exclusive queue bound to the fanout.
var latches = new List<TaskCompletionSource>();
for (var i = 0; i < subscribers; i++)
{
    var ch = await connection.CreateChannelAsync();
    await ch.ExchangeDeclareAsync(exchange, ExchangeType.Fanout, durable: false);
    var queue = (await ch.QueueDeclareAsync("", durable: false, exclusive: true, autoDelete: true)).QueueName;
    await ch.QueueBindAsync(queue, exchange, ""); // routing key ignored for fanout
    var done = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
    var got = 0;
    var consumer = new AsyncEventingBasicConsumer(ch);
    consumer.ReceivedAsync += (_, _) =>
    {
        if (Interlocked.Increment(ref got) == messages) done.TrySetResult();
        return Task.CompletedTask;
    };
    await ch.BasicConsumeAsync(queue, autoAck: true, consumer: consumer);
    Console.WriteLine($"[sub] bound exclusive queue {queue}");
    latches.Add(done);
}

// Publisher — broadcast on a confirm channel.
var confirmOpts = new CreateChannelOptions(
    publisherConfirmationsEnabled: true,
    publisherConfirmationTrackingEnabled: true);
await using var pub = await connection.CreateChannelAsync(confirmOpts);
await pub.ExchangeDeclareAsync(exchange, ExchangeType.Fanout, durable: false);
for (var i = 0; i < messages; i++)
    await pub.BasicPublishAsync(exchange, "", body: Encoding.UTF8.GetBytes($"log-{i}"));
Console.WriteLine($"[x] Broadcast {messages} messages to fanout '{exchange}'");

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await Task.WhenAll(latches.Select(l => l.Task.WaitAsync(cts.Token)));
Console.WriteLine("both subscribers received all broadcasts independently");
# frozen_string_literal: true
require "bunny"
require "amq/uri"

EXCHANGE = "logs"
MESSAGE_COUNT = 5

opts = AMQ::URI.parse(ENV.fetch("KUBEMQ_AMQP_URL", "amqp://guest:guest@localhost:5672/"))
opts[:vhost] = "/" if opts[:vhost].nil? || opts[:vhost].to_s.empty?
conn = Bunny.new(opts)
conn.start

pub_ch = conn.create_channel
pub_ch.confirm_select
exchange = pub_ch.fanout(EXCHANGE, durable: false)

# Two subscribers, each with its own server-named exclusive queue.
subscribers = Array.new(2) do |idx|
  sub_ch = conn.create_channel
  queue = sub_ch.queue("", exclusive: true) # amq.gen-*
  queue.bind(exchange) # routing key ignored for fanout
  received = Queue.new
  queue.subscribe(manual_ack: false, block: false) { |_di, _props, _body| received.push(:msg) }
  puts " [*] Subscriber #{idx + 1} bound exclusive queue #{queue.name}"
  { id: idx + 1, received: received }
end

# Broadcast 5 messages, then wait for confirms.
MESSAGE_COUNT.times { |i| exchange.publish("log-#{i}", routing_key: "ignored") }
pub_ch.wait_for_confirms
puts " [x] Broadcast #{MESSAGE_COUNT} messages to fanout '#{EXCHANGE}'"

# Each subscriber independently receives all 5.
subscribers.each do |s|
  MESSAGE_COUNT.times { s[:received].pop }
  puts " [x] Subscriber #{s[:id]} received all #{MESSAGE_COUNT} broadcasts"
end

exchange.delete
conn.close
use futures_lite::StreamExt;
use lapin::{
    options::{
        BasicConsumeOptions, BasicPublishOptions, ConfirmSelectOptions, ExchangeDeclareOptions,
        QueueBindOptions, QueueDeclareOptions,
    },
    types::FieldTable,
    BasicProperties, Connection, ConnectionProperties, ExchangeKind,
};

const EXCHANGE: &str = "logs";
const MESSAGES: usize = 5;

fn amqp_url() -> String {
    let url = std::env::var("KUBEMQ_AMQP_URL")
        .unwrap_or_else(|_| "amqp://guest:guest@localhost:5672/".into());
    match url.rsplit_once('@').map_or(url.as_str(), |(_, host)| host) {
        host if host.ends_with('/') && !host.to_lowercase().ends_with("/%2f") => format!("{url}%2f"),
        _ => url,
    }
}

async fn subscribe(conn: &Connection) -> Result<lapin::Consumer, Box<dyn std::error::Error>> {
    let ch = conn.create_channel().await?;
    ch.exchange_declare(EXCHANGE, ExchangeKind::Fanout, ExchangeDeclareOptions::default(), FieldTable::default())
        .await?;
    let queue = ch
        .queue_declare("", QueueDeclareOptions { exclusive: true, ..Default::default() }, FieldTable::default())
        .await?;
    ch.queue_bind(queue.name().as_str(), EXCHANGE, "", QueueBindOptions::default(), FieldTable::default())
        .await?;
    let consumer = ch
        .basic_consume(queue.name().as_str(), "", BasicConsumeOptions { no_ack: true, ..Default::default() }, FieldTable::default())
        .await?;
    println!("[sub] bound exclusive queue {}", queue.name());
    Ok(consumer)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let conn = Connection::connect(&amqp_url(), ConnectionProperties::default()).await?;

    let mut sub_a = subscribe(&conn).await?;
    let mut sub_b = subscribe(&conn).await?;

    // Publisher — broadcast on a confirm channel.
    let pub_ch = conn.create_channel().await?;
    pub_ch.exchange_declare(EXCHANGE, ExchangeKind::Fanout, ExchangeDeclareOptions::default(), FieldTable::default())
        .await?;
    pub_ch.confirm_select(ConfirmSelectOptions::default()).await?;
    for i in 0..MESSAGES {
        pub_ch
            .basic_publish(EXCHANGE, "", BasicPublishOptions::default(), format!("log-{i}").as_bytes(), BasicProperties::default())
            .await?
            .await?;
    }
    println!("[x] Broadcast {MESSAGES} messages to fanout '{EXCHANGE}'");

    for (label, consumer) in [("A", &mut sub_a), ("B", &mut sub_b)] {
        for _ in 0..MESSAGES {
            consumer.next().await.ok_or("subscriber stream closed early")??;
        }
        println!("[s{label}] received all {MESSAGES} broadcasts");
    }
    conn.close(0, "done").await?;
    Ok(())
}

Server-named exclusive queues

queue.declare("") makes the broker mint a unique name (amq.gen-{id}). Declaring it exclusive scopes it to the subscriber's connection and auto-deletes it on disconnect — exactly what you want for a transient subscriber. Each such queue is a normal KubeMQ Queue channel that lives only as long as the subscriber.

Confirm before closing a broadcast producer. Without publisher confirms, basic.publish only buffers the message on the connector's per-channel executor and returns; closing the channel or connection before that buffer drains silently abandons the un-ingested publishes — no error, no nack. A tight broadcast-then-close loop can lose most of a batch. Enable confirm.select (a confirm channel) and wait for all acks before closing, as every example above does, or keep the connection open until subscribers have drained.

Exclusive queues are node-local in a cluster. An exclusive (server-named) queue lives only on the node owning the subscriber's connection; the publisher must reach the same node for the broadcast to land. Single-node deployments are unaffected.

Was this page helpful?

On this page