Getting Started
Connect a stock STOMP client to KubeMQ and run a publish-and-subscribe round-trip over the Events pattern in minutes — no KubeMQ SDK required.
Get a message flowing through the KubeMQ STOMP connector in minutes. You point a standard STOMP
client at the broker, SUBSCRIBE to a /topic/ destination, SEND a message to a matching
destination, and watch it arrive — all over the native STOMP wire, with no KubeMQ SDK. This
walkthrough takes you from a running server to a verified pub/sub round-trip.
Prerequisites
- A running kubemq-server with the STOMP connector enabled and reachable on port 61613 (plain TCP). The connector is opt-in (disabled by default) — see the enable step below.
- One of the STOMP clients below for your language (the examples pin a native client per language — there is no KubeMQ SDK).
Enable the connector
The STOMP connector is disabled by default — a stock kubemq-server does not bind the STOMP listener until you turn it on. Enable it with its enable variable:
docker run -d \ --name kubemq \ -p 61613:61613 \ -p 61614:61614 \ -p 50000:50000 \ -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY \ -e CONNECTORS_STOMP_ENABLE=true \ europe-docker.pkg.dev/kubemq/images/kubemq:nextThe enable variable is CONNECTORS_STOMP_ENABLE — spell it verbatim, with the underscore
between CONNECTORS and STOMP. The form CONNECTORSSTOMP_ENABLE (without underscore)
does not bind and is silently ignored. For Kubernetes, set spec.stomp.enabled: true
in the KubemqCluster CR.
Bring up a throwaway local broker with STOMP enabled:
Every example reads a single environment variable for the broker endpoint. The scheme selects
the transport — tcp:// (61613) or tls:// (61614):
# default: tcp://localhost:61613
export KUBEMQ_STOMP_URL="tcp://localhost:61613"Verify the listener is actually up. The connector loader is availability-first — if the
port fails to bind, the server logs an error and keeps running without STOMP rather than
crashing. Do not infer the listener from a successful server boot: confirm it via the /stomp
dashboard, the kubemq_stomp_connections Prometheus gauge, or
GET /api/stomp/connections. See
Connections endpoint.
To disable the STOMP connector after enabling it, set its enable variable to false:
docker run -d -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY -e CONNECTORS_STOMP_ENABLE=false europe-docker.pkg.dev/kubemq/images/kubemq:nextWhen Enable is false, no STOMP listener binds and the rest of the STOMP config is skipped.
See Configuration for the full settings list.
How it works
Every connection opens with one CONNECT frame that negotiates the protocol version and
heartbeat; the connector replies with CONNECTED. A subscriber registers a /topic/
destination; a publisher SENDs to a destination with the same prefix. The connector resolves
the prefix (/topic/) to the Events pattern, joins the remaining segments with . for the
channel, and delivers every message to matching subscribers.
A SEND to /topic/demo maps to the Events channel demo; the connector fans the message out as a MESSAGE frame to every subscriber on that destination.
Steps
Connect to the broker
Open a STOMP connection to the endpoint in KUBEMQ_STOMP_URL. The connector negotiates the
highest common version of 1.0/1.1/1.2; the examples request accept-version:1.2. In the default
no-auth mode any login / passcode works (including empty) — login is freeform and only
derives the session id when there are no auth claims.
The language tabs across all three steps run the complete round-trip from a single program:
connect a subscriber and a publisher, subscribe to /topic/demo, send one message, and confirm
the subscriber receives it.
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/go-stomp/stomp/v3"
)
func stompAddr() string {
url := os.Getenv("KUBEMQ_STOMP_URL")
if url == "" {
url = "tcp://localhost:61613"
}
return url[len("tcp://"):]
}
func dial(login string) (*stomp.Conn, error) {
return stomp.Dial("tcp", stompAddr(),
stomp.ConnOpt.AcceptVersion(stomp.V12),
stomp.ConnOpt.Login(login, ""))
}
func main() {
const destination = "/topic/demo" // Events pattern, channel "demo"
// SUBSCRIBER — connect and subscribe before publishing (Events is at-most-once).
sub, err := dial("go-sub")
if err != nil {
log.Fatalf("sub connect: %v", err)
}
subscription, err := sub.Subscribe(destination, stomp.AckAuto)
if err != nil {
log.Fatalf("subscribe: %v", err)
}
fmt.Printf("[sub] subscribed to %s\n", destination)
time.Sleep(300 * time.Millisecond) // let the subscription register
// PUBLISHER — connect and SEND one message.
pub, err := dial("go-pub")
if err != nil {
log.Fatalf("pub connect: %v", err)
}
if err := pub.Send(destination, "application/json",
[]byte(`{"message":"hello"}`), stomp.SendOpt.Receipt); err != nil {
log.Fatalf("send: %v", err)
}
fmt.Printf("[pub] sent to %s\n", destination)
// RECEIVE — wait for the MESSAGE frame.
select {
case msg := <-subscription.C:
fmt.Printf("[sub] received: %s (destination=%s)\n", string(msg.Body), msg.Destination)
case <-time.After(10 * time.Second):
log.Fatal("timed out waiting for the event")
}
_ = pub.Disconnect()
_ = sub.Disconnect()
}import os
import queue
import time
import stomp
def endpoint() -> tuple[str, int]:
url = os.environ.get("KUBEMQ_STOMP_URL", "tcp://localhost:61613")
host, port = url.split("://", 1)[1].split(":", 1)
return host, int(port)
class Listener(stomp.ConnectionListener):
def __init__(self) -> None:
self.messages: queue.Queue = queue.Queue()
def on_message(self, frame) -> None:
self.messages.put(frame)
def connect(login: str, listener: stomp.ConnectionListener | None = None) -> stomp.Connection:
conn = stomp.Connection([endpoint()], heartbeats=(10000, 10000))
if listener is not None:
conn.set_listener("", listener)
conn.connect(login=login, passcode="", wait=True)
return conn
def main() -> None:
destination = "/topic/demo" # Events pattern, channel "demo"
# SUBSCRIBER — connect and subscribe first.
listener = Listener()
sub = connect("py-sub", listener)
sub.subscribe(destination=destination, id="sub-1", ack="auto")
print(f"[sub] subscribed to {destination}")
time.sleep(0.3)
# PUBLISHER — connect and SEND.
pub = connect("py-pub")
pub.send(destination=destination, body='{"message":"hello"}',
content_type="application/json")
print(f"[pub] sent to {destination}")
frame = listener.messages.get(timeout=10)
body = frame.body if isinstance(frame.body, str) else frame.body.decode()
print(f"[sub] received: {body} (destination={frame.headers['destination']})")
pub.disconnect()
sub.disconnect()
if __name__ == "__main__":
main()import java.net.URI;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.lang.reflect.Type;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.messaging.simp.stomp.ReactorNettyTcpStompClient;
public final class Main {
public static void main(String[] args) throws Exception {
String url = System.getenv().getOrDefault("KUBEMQ_STOMP_URL", "tcp://localhost:61613");
URI uri = URI.create(url);
String destination = "/topic/demo"; // Events pattern, channel "demo"
ReactorNettyTcpStompClient client =
new ReactorNettyTcpStompClient(uri.getHost(), uri.getPort());
LinkedBlockingQueue<String> inbox = new LinkedBlockingQueue<>();
// SUBSCRIBER
StompSession sub = client.connect(new StompSessionHandlerAdapter() {}).get();
sub.subscribe(destination, new StompSessionHandlerAdapter() {
@Override public Type getPayloadType(StompHeaders headers) { return byte[].class; }
@Override public void handleFrame(StompHeaders headers, Object payload) {
inbox.add(new String((byte[]) payload));
}
});
System.out.printf("[sub] subscribed to %s%n", destination);
Thread.sleep(300);
// PUBLISHER
StompSession pub = client.connect(new StompSessionHandlerAdapter() {}).get();
StompHeaders headers = new StompHeaders();
headers.setDestination(destination);
headers.add("content-type", "application/json");
pub.send(headers, "{\"message\":\"hello\"}".getBytes());
System.out.printf("[pub] sent to %s%n", destination);
String body = inbox.poll(10, TimeUnit.SECONDS);
System.out.printf("[sub] received: %s%n", body);
pub.disconnect();
sub.disconnect();
client.shutdown();
}
}import { connect, type Client } from "stompit";
function endpoint(): { host: string; port: number } {
const url = new URL(process.env["KUBEMQ_STOMP_URL"] ?? "tcp://localhost:61613");
return { host: url.hostname, port: Number(url.port) || 61613 };
}
function dial(login: string): Promise<Client> {
const { host, port } = endpoint();
return new Promise((resolve, reject) => {
connect({ host, port, connectHeaders: { "accept-version": "1.2",
"heart-beat": "10000,10000", login } },
(err, c) => (err ? reject(err) : resolve(c)));
});
}
async function main(): Promise<void> {
const destination = "/topic/demo"; // Events pattern, channel "demo"
// SUBSCRIBER
const sub = await dial("js-sub");
const received = new Promise<string>((resolve, reject) => {
sub.subscribe({ destination, ack: "auto" }, (err, message) => {
if (err) return reject(err);
message.readString("utf-8", (e, body) => (e ? reject(e) : resolve(body ?? "")));
});
});
console.log(`[sub] subscribed to ${destination}`);
await new Promise((r) => setTimeout(r, 300));
// PUBLISHER
const pub = await dial("js-pub");
const frame = pub.send({ destination, "content-type": "application/json" });
frame.write('{"message":"hello"}');
frame.end();
console.log(`[pub] sent to ${destination}`);
console.log(`[sub] received: ${await received}`);
pub.disconnect();
sub.disconnect();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});using System.Text;
using Apache.NMS;
using Stomp.Net;
static string BrokerUri()
{
var url = Environment.GetEnvironmentVariable("KUBEMQ_STOMP_URL") ?? "tcp://localhost:61613";
var u = new Uri(url);
var transport = u.Scheme == "tls" ? "ssl" : "tcp";
return $"{transport}://{u.Host}:{(u.Port > 0 ? u.Port : 61613)}";
}
const string destination = "/topic/demo"; // Events pattern, channel "demo"
var factory = new ConnectionFactory(BrokerUri(), new StompConnectionSettings());
var received = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
// SUBSCRIBER
using var subConn = factory.CreateConnection();
subConn.Start();
using var subSession = subConn.CreateSession(AcknowledgementMode.AutoAcknowledge);
using var consumer = subSession.CreateConsumer(subSession.GetTopic(destination));
consumer.Listener += msg =>
received.TrySetResult(Encoding.UTF8.GetString(((IBytesMessage)msg).Content));
Console.WriteLine($"[sub] subscribed to {destination}");
await Task.Delay(300);
// PUBLISHER
using var pubConn = factory.CreateConnection();
pubConn.Start();
using var pubSession = pubConn.CreateSession(AcknowledgementMode.AutoAcknowledge);
using var producer = pubSession.CreateProducer(pubSession.GetTopic(destination));
var message = pubSession.CreateBytesMessage(Encoding.UTF8.GetBytes("{\"message\":\"hello\"}"));
message.StompType = "application/json";
producer.Send(message);
Console.WriteLine($"[pub] sent to {destination}");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
Console.WriteLine($"[sub] received: {await received.Task.WaitAsync(cts.Token)}");require "stomp"
require "uri"
require "timeout"
uri = URI.parse(ENV.fetch("KUBEMQ_STOMP_URL", "tcp://localhost:61613"))
destination = "/topic/demo" # Events pattern, channel "demo"
def connect(uri, login)
Stomp::Client.new(
hosts: [{ host: uri.host, port: uri.port }],
connect_headers: { "accept-version" => "1.2", "heart-beat" => "10000,10000",
"login" => login, "passcode" => "" },
)
end
inbox = Queue.new
# SUBSCRIBER
sub = connect(uri, "rb-sub")
sub.subscribe(destination, id: "sub-1", ack: "auto") { |msg| inbox << msg.body }
puts "[sub] subscribed to #{destination}"
sleep 0.3
# PUBLISHER
pub = connect(uri, "rb-pub")
pub.publish(destination, '{"message":"hello"}', { "content-type" => "application/json" })
puts "[pub] sent to #{destination}"
body = Timeout.timeout(10) { inbox.pop }
puts "[sub] received: #{body}"
pub.close
sub.closeuse async_stomp::client::Connector;
use async_stomp::{FromServer, ToServer};
use futures::{SinkExt, StreamExt};
use std::time::Duration;
use tokio::time::timeout;
async fn dial(host_port: &str, login: &str) -> Result<
impl SinkExt<async_stomp::Message<ToServer>> + StreamExt, Box<dyn std::error::Error>> {
Ok(Connector::builder()
.server(host_port)
.login(login.to_string())
.passcode(String::new())
.connect()
.await?)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = std::env::var("KUBEMQ_STOMP_URL").unwrap_or_else(|_| "tcp://localhost:61613".into());
let host_port = url.split("://").nth(1).unwrap_or("localhost:61613").to_string();
let destination = "/topic/demo"; // Events pattern, channel "demo"
// SUBSCRIBER
let mut sub = Connector::builder().server(&host_port)
.login("rust-sub".into()).passcode(String::new()).connect().await?;
sub.send(ToServer::Subscribe {
destination: destination.to_string(),
id: "sub-1".to_string(),
ack: None,
}).await?;
println!("[sub] subscribed to {destination}");
tokio::time::sleep(Duration::from_millis(300)).await;
// PUBLISHER
let mut pubc = Connector::builder().server(&host_port)
.login("rust-pub".into()).passcode(String::new()).connect().await?;
pubc.send(ToServer::Send {
destination: destination.to_string(),
transaction: None,
headers: Some(vec![("content-type".into(), "application/json".into())]),
body: Some(br#"{"message":"hello"}"#.to_vec()),
}).await?;
println!("[pub] sent to {destination}");
// RECEIVE
if let Ok(Some(Ok(msg))) = timeout(Duration::from_secs(10), sub.next()).await {
if let FromServer::Message { body, .. } = msg.content {
println!("[sub] received: {}", String::from_utf8_lossy(&body.unwrap_or_default()));
}
}
pubc.send(ToServer::Disconnect { receipt: None }).await?;
Ok(())
}Send a message
The publisher in the program above SENDs one message to /topic/demo. The prefix /topic/
selects the Events pattern, and the remaining segments become the KubeMQ channel with /
translated to . — so /topic/demo lands on channel demo. Attaching a receipt: header
makes the connector return a RECEIPT once it has accepted the frame (this confirms
acceptance, not consumer delivery).
Subscribe and verify
The subscriber registers the destination /topic/demo. When the message arrives it is
delivered as a MESSAGE frame whose destination header is canonicalized to the primary form,
and the program prints it and exits:
[sub] subscribed to /topic/demo
[pub] sent to /topic/demo
[sub] received: {"message":"hello"} (destination=/topic/demo)Events is fire-and-forget pub/sub: subscribe before you publish, or the message is gone. For
persistence and replay-on-reconnect, use the Events-Store pattern (/topic-store/) instead.
Wildcard subscriptions are accepted on the Events pattern only, are subscribe-only, and
use the message broker's native wildcard syntax (* = one segment, > = the final tail) —
there is no MQTT-style +/#. Avoid a literal . in a destination segment: /topic/a.b and
/topic/a/b both map to channel a.b, and egress always emits the slash form. See
Destination mapping.
Next steps
Configuration
The 11 connector settings, the CONNECTORS_STOMP_ENABLE spelling, ports, and validation.
Architecture
The embedded STOMP server, the frame codec, version negotiation, and the destination router.
Events
Fire-and-forget pub/sub over STOMP — fan-out, wildcards, and the slash→dot mapping.
Queues
Competing-consumer queues over STOMP with ack modes, receipts, and redelivery.
Was this page helpful?
Configuration
How the STOMP connector's opt-in enable flag, DefaultPattern, ports, and TLS behavior fit together in KubeMQ.
Ack modes and receipts
STOMP delivery reliability on KubeMQ — the auto, client-individual, and client ack modes, ACK/NACK correlation, the 30s requeue timeout, and RECEIPT frames.