Getting Started
Connect a stock AMQP 1.0 client to KubeMQ and run a send-and-receive round-trip through a KubeMQ Queue in minutes — no KubeMQ SDK required.
Get a message flowing through the KubeMQ AMQP 1.0 connector in minutes. You point a
standard AMQP 1.0 client at the broker, attach a sender to queues/<channel>, produce a
few messages, then attach a receiver and consume them back — all over the native AMQP 1.0
wire, with no KubeMQ SDK. This walkthrough takes you from a running server to a verified
at-least-once round-trip.
Prerequisites
- A running kubemq-server with the AMQP 1.0 connector enabled and reachable on port 5672 (plain TCP). The connector is opt-in (disabled by default) — see the enable step below.
- One of the AMQP 1.0 clients below for your language (the examples pin a native client per language — there is no KubeMQ SDK).
Enable the connector
The AMQP 1.0 connector is disabled by default — a stock kubemq-server does not bind the AMQP 1.0 listener until you turn it on. Enable it with its enable variable:
docker run -d \ --name kubemq \ -p 5672:5672 \ -p 5671:5671 \ -p 50000:50000 \ -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY \ -e CONNECTORS_AMQP10_ENABLE=true \ europe-docker.pkg.dev/kubemq/images/kubemq:nextThe enable variable is CONNECTORS_AMQP10_ENABLE — the literal 10 stays attached to
AMQP with no underscore. CONNECTORS_AMQP_1_0_ENABLE and CONNECTORS_AMQP10ENABLE do
not bind. For Kubernetes, set spec.amqp10.enabled: true in the KubemqCluster CR.
Bring up a throwaway local broker with AMQP 1.0 enabled:
Every example reads a single environment variable for the broker endpoint. A URL with no userinfo negotiates SASL ANONYMOUS, so a stock dev broker is clone-and-run with no credentials:
# default: amqp://localhost:5672
export KUBEMQ_AMQP_URL="amqp://localhost:5672"To disable the AMQP 1.0 connector after enabling it, set its enable variable to false:
docker run -d -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY -e CONNECTORS_AMQP10_ENABLE=false europe-docker.pkg.dev/kubemq/images/kubemq:nextWhen Enable is false, all other AMQP 1.0 validation is skipped and no listener binds. See
Configuration for the full settings list.
How it works
A producer attaches a sender link to a node address; the connector resolves the address prefix to a KubeMQ pattern and channel and enqueues each message. A consumer attaches a receiver link to the same address, grants credit, and the connector delivers and removes each accepted message.
A sender produces unsettled messages to a Queue channel; a receiver grants credit, and the connector delivers and removes each accepted message.
Steps
Connect to the broker
Open an AMQP 1.0 connection to the endpoint in KUBEMQ_AMQP_URL. A URL with no userinfo
negotiates SASL ANONYMOUS; the client sends a non-empty container-id automatically (the
connector requires one). One session carries both the producer and consumer links in the
steps below.
The language tabs across all three steps run the complete round-trip from a single
program: connect, produce 10 messages to queues/amqp10.examples.basic, consume and
accept each, and confirm the queue drains to empty.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
amqp "github.com/Azure/go-amqp"
)
const channel = "amqp10.examples.basic"
const total = 10
func amqpURL() string {
if v := os.Getenv("KUBEMQ_AMQP_URL"); v != "" {
return v
}
return "amqp://localhost:5672"
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
addr := "queues/" + channel
// CONNECT: OPEN (SASL ANONYMOUS) + BEGIN one session.
conn, err := amqp.Dial(ctx, amqpURL(), nil)
if err != nil {
log.Fatalf("dial: %v", err)
}
defer func() { _ = conn.Close() }()
session, err := conn.NewSession(ctx, nil)
if err != nil {
log.Fatalf("new session: %v", err)
}
// SEND: attach a sender; each unsettled Send blocks for the accepted disposition.
sender, err := session.NewSender(ctx, addr, nil)
if err != nil {
log.Fatalf("new sender: %v", err)
}
for i := 0; i < total; i++ {
body := fmt.Sprintf("msg-%03d", i)
if err := sender.Send(ctx, amqp.NewMessage([]byte(body)), nil); err != nil {
log.Fatalf("send %s: %v", body, err)
}
}
_ = sender.Close(ctx)
fmt.Printf("[send] Produced %d messages to %s\n", total, addr)
// RECEIVE: attach a receiver with credit; accept each => removed from the queue.
receiver, err := session.NewReceiver(ctx, addr, &amqp.ReceiverOptions{Credit: 10})
if err != nil {
log.Fatalf("new receiver: %v", err)
}
seen := make(map[string]struct{}, total)
for len(seen) < total {
msg, err := receiver.Receive(ctx, nil)
if err != nil {
log.Fatalf("receive: %v", err)
}
if err := receiver.AcceptMessage(ctx, msg); err != nil {
log.Fatalf("accept: %v", err)
}
seen[string(msg.GetData())] = struct{}{}
}
fmt.Printf("[recv] Consumed and accepted %d messages (no loss)\n", len(seen))
_ = receiver.Close(ctx)
}import os
from proton import Message
from proton.utils import BlockingConnection
CHANNEL = "amqp10.examples.basic"
TOTAL = 10
def amqp_url() -> str:
return os.environ.get("KUBEMQ_AMQP_URL", "amqp://localhost:5672")
def main() -> None:
addr = "queues/" + CHANNEL
# CONNECT: OPEN (SASL ANONYMOUS) — proton sends a non-empty container-id.
conn = BlockingConnection(amqp_url())
try:
# SEND: attach a sender; each send blocks for the accepted disposition.
sender = conn.create_sender(addr)
for i in range(TOTAL):
sender.send(Message(body=f"msg-{i:03d}"))
sender.close()
print(f"[send] Produced {TOTAL} messages to {addr}")
# RECEIVE: attach a receiver with credit; accept each => removed from the queue.
receiver = conn.create_receiver(addr, credit=10)
seen: set[str] = set()
while len(seen) < TOTAL:
msg = receiver.receive(timeout=30.0)
receiver.accept()
seen.add(str(msg.body))
print(f"[recv] Consumed and accepted {len(seen)} messages (no loss)")
receiver.close()
finally:
conn.close()
if __name__ == "__main__":
main()import java.util.HashSet;
import java.util.Set;
import javax.jms.Connection;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import org.apache.qpid.jms.JmsConnectionFactory;
public final class Main {
private static final String CHANNEL = "amqp10.examples.basic";
private static final int TOTAL = 10;
public static void main(String[] args) throws Exception {
String url = System.getenv().getOrDefault("KUBEMQ_AMQP_URL", "amqp://localhost:5672");
String address = "queues/" + CHANNEL;
// CONNECT: OPEN (SASL ANONYMOUS). The JMS destination name IS the node address.
JmsConnectionFactory factory = new JmsConnectionFactory(url);
try (Connection connection = factory.createConnection()) {
connection.start();
try (Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE)) {
Queue queue = session.createQueue(address);
// SEND: a producer is a server-receiver link; send blocks for accepted.
try (MessageProducer producer = session.createProducer(queue)) {
for (int i = 0; i < TOTAL; i++) {
producer.send(session.createTextMessage(String.format("msg-%03d", i)));
}
}
System.out.printf("[send] Produced %d messages to %s%n", TOTAL, address);
// RECEIVE: a CLIENT_ACKNOWLEDGE consumer; acknowledge() settles accepted.
try (MessageConsumer consumer = session.createConsumer(queue)) {
Set<String> seen = new HashSet<>();
while (seen.size() < TOTAL) {
Message msg = consumer.receive(30_000);
if (msg == null) {
throw new IllegalStateException("timed out before draining the queue");
}
String body = msg.getBody(String.class);
msg.acknowledge();
seen.add(body);
}
System.out.printf("[recv] Consumed and accepted %d messages (no loss)%n", seen.size());
}
}
}
}
}using System.Text;
using Amqp;
using Amqp.Framing;
const string channel = "amqp10.examples.basic";
const int total = 10;
static string AmqpUrl() =>
Environment.GetEnvironmentVariable("KUBEMQ_AMQP_URL") is { Length: > 0 } v
? v
: "amqp://localhost:5672";
var addr = "queues/" + channel;
// CONNECT: OPEN (SASL ANONYMOUS) + one session.
var connection = await Connection.Factory.CreateAsync(new Address(AmqpUrl()));
try
{
var session = new Session(connection);
// SEND: a SenderLink is a server-receiver link; Send blocks for accepted.
var sender = new SenderLink(session, "basic-sender", addr);
for (var i = 0; i < total; i++)
{
var message = new Message
{
BodySection = new Data { Binary = Encoding.UTF8.GetBytes($"msg-{i:D3}") },
};
sender.Send(message, TimeSpan.FromSeconds(15));
}
Console.WriteLine($"[send] Produced {total} messages to {addr}");
// RECEIVE: grant credit; Accept each => removed from the queue. Keep the
// sender open through the consume phase (detaching it early can stall delivery).
var receiver = new ReceiverLink(session, "basic-receiver", addr);
receiver.SetCredit(10, autoRestore: true);
var seen = new HashSet<string>();
while (seen.Count < total)
{
var message = receiver.Receive(TimeSpan.FromSeconds(30))
?? throw new InvalidOperationException("receive timed out");
receiver.Accept(message);
seen.Add(BodyString(message));
}
Console.WriteLine($"[recv] Consumed and accepted {seen.Count} messages (no loss)");
await receiver.CloseAsync();
await sender.CloseAsync();
await session.CloseAsync();
}
finally
{
await connection.CloseAsync();
}
static string BodyString(Message message) => message.BodySection switch
{
Data d => Encoding.UTF8.GetString(d.Binary),
AmqpValue { Value: byte[] bytes } => Encoding.UTF8.GetString(bytes),
AmqpValue { Value: string str } => str,
AmqpValue v => v.Value?.ToString() ?? string.Empty,
_ => string.Empty,
};import {
Connection,
ReceiverEvents,
type ConnectionOptions,
type EventContext,
type Receiver,
} from "rhea-promise";
const channel = "amqp10.examples.basic";
const total = 10;
function connectionOptions(): ConnectionOptions {
const url = new URL(process.env["KUBEMQ_AMQP_URL"] ?? "amqp://localhost:5672");
return {
host: url.hostname,
port: url.port ? Number(url.port) : 5672,
container_id: `kubemq-amqp10-js-${process.pid}`,
reconnect: false,
};
}
function bodyToString(body: unknown): string {
return Buffer.isBuffer(body) ? body.toString("utf8") : String(body);
}
async function main(): Promise<void> {
const address = `queues/${channel}`;
// CONNECT: OPEN (SASL ANONYMOUS).
const connection = new Connection(connectionOptions());
await connection.open();
try {
// SEND: an AwaitableSender; each send() resolves on the accepted disposition.
const sender = await connection.createAwaitableSender({ target: { address } });
for (let i = 0; i < total; i++) {
await sender.send({ body: `msg-${String(i).padStart(3, "0")}` }, { timeoutInSeconds: 15 });
}
await sender.close();
console.log(`[send] Produced ${total} messages to ${address}`);
// RECEIVE: manual credit + manual settle; accept() removes from the queue.
const receiver = await connection.createReceiver({
source: { address },
credit_window: 0,
autoaccept: false,
autosettle: false,
});
const seen = new Set<string>();
await new Promise<void>((resolve, reject) => {
receiver.on(ReceiverEvents.message, (ctx: EventContext) => {
ctx.delivery?.accept();
seen.add(bodyToString(ctx.message?.body));
if (seen.size >= total) resolve();
});
receiver.on(ReceiverEvents.receiverError, (ctx: EventContext) =>
reject(ctx.receiver?.error ?? new Error("receiver error")));
(receiver as Receiver).addCredit(total);
});
console.log(`[recv] Consumed and accepted ${seen.size} messages (no loss)`);
await receiver.close();
} finally {
await connection.close();
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});use std::collections::HashSet;
use fe2o3_amqp::link::delivery::Delivery;
use fe2o3_amqp::link::receiver::CreditMode;
use fe2o3_amqp::{Connection, Receiver, Sender, Session};
use fe2o3_amqp_types::definitions::SenderSettleMode;
use fe2o3_amqp_types::messaging::{Body, Message};
use fe2o3_amqp_types::primitives::Value;
const CHANNEL: &str = "amqp10.examples.basic";
const TOTAL: usize = 10;
fn amqp_url() -> String {
std::env::var("KUBEMQ_AMQP_URL").unwrap_or_else(|_| "amqp://localhost:5672".to_string())
}
fn body_string(msg: &Message<Body<Value>>) -> String {
match &msg.body {
Body::Data(batch) => batch.iter().flat_map(|d| d.0.iter().copied()).collect::<Vec<u8>>(),
Body::Value(v) => match &v.0 {
Value::Binary(b) => b.to_vec(),
Value::String(s) => s.clone().into_bytes(),
other => format!("{other:?}").into_bytes(),
},
_ => Vec::new(),
}
.into_iter()
.map(|b| b as char)
.collect()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = format!("queues/{CHANNEL}");
// CONNECT: OPEN (SASL ANONYMOUS) + BEGIN one session.
let mut connection = Connection::open("amqp10-examples-basic", amqp_url().as_str()).await?;
let mut session = Session::begin(&mut connection).await?;
// SEND: a sender pinned to Unsettled (at-least-once); the default `mixed` is rejected.
let mut sender = Sender::builder()
.name("basic-sender")
.target(addr.as_str())
.sender_settle_mode(SenderSettleMode::Unsettled)
.attach(&mut session)
.await?;
for i in 0..TOTAL {
let outcome = sender.send(format!("msg-{i:03}")).await?;
if !outcome.is_accepted() {
return Err(format!("send {i}: unexpected outcome {outcome:?}").into());
}
}
sender.close().await?;
println!("[send] Produced {TOTAL} messages to {addr}");
// RECEIVE: client-granted auto credit; accept each => removed from the queue.
let mut receiver = Receiver::builder()
.name("basic-receiver")
.source(addr.as_str())
.credit_mode(CreditMode::Auto(10))
.attach(&mut session)
.await?;
let mut seen: HashSet<String> = HashSet::with_capacity(TOTAL);
while seen.len() < TOTAL {
let delivery: Delivery<Body<Value>> = receiver.recv().await?;
receiver.accept(&delivery).await?;
seen.insert(body_string(delivery.message()));
}
println!("[recv] Consumed and accepted {} messages (no loss)", seen.len());
receiver.close().await?;
session.end().await?;
connection.close().await?;
Ok(())
}Send messages
The send phase in the program above attaches a sender link with the target address
queues/amqp10.examples.basic and produces 10 messages. Because the sends are unsettled,
each call blocks until the connector returns an accepted disposition — confirmation that
the broker stored the message (at-least-once). Always use the explicit pattern prefix
(queues/…); never rely on the connector's bare-address fallback.
Receive and verify
The receive phase attaches a receiver link to the same address and grants link credit;
the connector delivers each message, and accept settles it — emitting an AckRange that
removes it from the queue. After consuming all 10, a further receive times out, proving the
queue drained with no loss:
[send] Produced 10 messages to queues/amqp10.examples.basic
[recv] Consumed and accepted 10 messages (no loss)
[recv] Queue drained to empty (no further messages)Behind that, the full AMQP 1.0 handshake ran end-to-end: OPEN → BEGIN → ATTACH (sender, then receiver) → TRANSFER / FLOW / DISPOSITION → DETACH / CLOSE.
Two flow-control footguns to internalize early: Events drop silently at zero credit (keep a consumer's credit topped up), and Events-Store stalls and loses its window if a durable consumer's credit is not replenished. Both are covered in Flow control.
Next steps
Configuration
The 14 connector settings, the CONNECTORS_AMQP10_ENABLE disable var, defaults, and validation.
Architecture
The amqpmux front door, the connection/session/link model, and the metadata envelope.
Queues
At-least-once enqueue and credit-driven destructive consume over AMQP 1.0.
Address mapping
The full address grammar, longest-prefix matching, and the channel charset rules.
Was this page helpful?
Queues
Durable competing-consumer work queues over AMQP 1.0 — at-least-once delivery, credit-driven consume, and settlement on the KubeMQ Queues pattern.
Authentication
How an AMQP 1.0 client authenticates to KubeMQ — SASL PLAIN with a KubeMQ JWT, SASL EXTERNAL with mTLS, ANONYMOUS for dev, and Casbin authorization.