STOMP
Point a STOMP app at KubeMQ by changing only the broker address — all five KubeMQ patterns over the STOMP 1.0/1.1/1.2 wire, selected by destination prefix.
Point your existing STOMP application at KubeMQ by changing only the broker address. The STOMP connector is a built-in, wire-protocol bridge inside kubemq-server — an embedded STOMP server with its own dedicated TCP/TLS listeners and a hand-rolled frame codec. Any standard, unmodified STOMP client (go-stomp, stomp.py, Stomp.Net, Spring) talks to KubeMQ's Queues, Events, Events-Store, Commands, and Queries with no code change, no library swap, and no KubeMQ SDK.
What is the STOMP connector
STOMP (Simple Text Oriented Messaging Protocol) is a frame-based
text protocol: a client opens a connection with a CONNECT frame, then SENDs to and
SUBSCRIBEs on destinations. The KubeMQ STOMP connector negotiates STOMP 1.0, 1.1, and
1.2 (it picks the highest common version; the examples default to 1.2) over raw TCP, and
maps the STOMP wire protocol onto KubeMQ's five native messaging patterns by destination
prefix.
The first path segment of a destination selects the pattern; the remaining segments are joined
with . into the KubeMQ channel — /topic/orders/new becomes Events channel orders.new. The
connector is a gateway, not a client library: your application only needs a stock STOMP
client.
Key capabilities:
- All five patterns over one wire — Queues, Events, Events-Store, Commands, and Queries, selected by the destination prefix.
- ActiveMQ-style primary names, MQTT-style aliases — lead with
/queue/,/topic/,/topic-store/,/command/,/query/; the aliases/queues/,/events/,/store/,/commands/,/queries/resolve to the same patterns, and egress always canonicalizes back to the primary name. - RPC requester-only — a STOMP client
SENDs to/command/or/query/and receives the reply on a connection-local/reply/subscription; the responder runs on the KubeMQ (gRPC) side. - Cross-protocol interop — a message sent over STOMP to
/topic/orders/newis consumable by a gRPC or REST KubeMQ client on channelorders.new, and vice-versa.
How it works
A STOMP client connects to the connector and SENDs to a destination. The connector resolves
the destination to a KubeMQ (pattern, channel) pair, hands the message to the message broker,
and consumers on the same channel — over STOMP or any other KubeMQ transport — receive it.
The connector parses the destination prefix into a KubeMQ pattern and joins the remaining segments (slash→dot) into the channel orders.new, then bridges onto the shared KubeMQ array.
Ports & protocol surface
| Port | Transport | Protocol | Notes |
|---|---|---|---|
61613 | Plain TCP | STOMP 1.0 / 1.1 / 1.2 | Default plain listener; binds all interfaces. |
61614 | TLS over TCP | STOMP 1.0 / 1.1 / 1.2 | Binds only when the server-wide Security block resolves to TLS. Must differ from the plain port. |
There is no STOMP-over-WebSocket listener — the connector speaks raw TCP only, so a
WebSocket-only client (such as @stomp/stompjs) cannot drive it. TLS has no STOMP-specific
configuration: certificate material, mTLS, and the minimum TLS version come from the
server-wide Security block. Connecting over TLS is purely a transport swap
(tls://host:61614); the STOMP frames on top are identical. See
Architecture for the protocol stack.
Send a message
The example below produces one message to a Queue over a stock STOMP client. The /queue/
prefix selects the Queues pattern (competing consumer, at-least-once); the remaining
segments become the KubeMQ channel with / translated to . — /queue/orders/new lands on
channel orders.new. Every client reads the broker endpoint from KUBEMQ_STOMP_URL (default
tcp://localhost:61613); the scheme selects the transport.
package main
import (
"fmt"
"log"
"os"
"github.com/go-stomp/stomp/v3"
)
func stompURL() string {
if v := os.Getenv("KUBEMQ_STOMP_URL"); v != "" {
return v
}
return "tcp://localhost:61613"
}
func main() {
// The scheme in KUBEMQ_STOMP_URL selects the transport; strip it for net.Dial.
addr := stompURL()[len("tcp://"):]
const destination = "/queue/orders/new" // Queues pattern, channel "orders.new"
// CONNECT: accept-version 1.2, default heart-beat, no-auth.
conn, err := stomp.Dial("tcp", addr,
stomp.ConnOpt.AcceptVersion(stomp.V12),
stomp.ConnOpt.Login("my-app", ""))
if err != nil {
log.Fatalf("connect: %v", err)
}
defer conn.Disconnect() //nolint:errcheck
// SEND one message; the receipt blocks until KubeMQ accepts the frame.
if err := conn.Send(destination, "text/plain",
[]byte("hello from STOMP"), stomp.SendOpt.Receipt); err != nil {
log.Fatalf("send: %v", err)
}
fmt.Printf("sent 1 message to %s (channel orders.new)\n", destination)
}import os
import stomp
def stomp_endpoint() -> tuple[str, int]:
url = os.environ.get("KUBEMQ_STOMP_URL", "tcp://localhost:61613")
host_port = url.split("://", 1)[1]
host, port = host_port.split(":", 1)
return host, int(port)
def main() -> None:
destination = "/queue/orders/new" # Queues pattern, channel "orders.new"
host, port = stomp_endpoint()
# CONNECT: accept-version 1.2, default heart-beat, no-auth.
conn = stomp.Connection([(host, port)], heartbeats=(10000, 10000))
conn.connect(login="my-app", passcode="", wait=True)
try:
conn.send(destination=destination, body="hello from STOMP",
content_type="text/plain")
print(f"sent 1 message to {destination} (channel orders.new)")
finally:
conn.disconnect()
if __name__ == "__main__":
main()import java.net.URI;
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.tcp.reactor.ReactorNettyTcpClient;
import org.springframework.web.socket.messaging.WebSocketStompClient;
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 = "/queue/orders/new"; // Queues pattern, channel "orders.new"
// CONNECT over raw TCP; Spring negotiates STOMP 1.2 by default.
ReactorNettyTcpStompClient client =
new ReactorNettyTcpStompClient(uri.getHost(), uri.getPort());
StompSession session =
client.connect(new StompSessionHandlerAdapter() {}).get();
StompHeaders headers = new StompHeaders();
headers.setDestination(destination);
headers.add("content-type", "text/plain");
session.send(headers, "hello from STOMP".getBytes());
System.out.printf("sent 1 message to %s (channel orders.new)%n", destination);
session.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 };
}
async function main(): Promise<void> {
const destination = "/queue/orders/new"; // Queues pattern, channel "orders.new"
const { host, port } = endpoint();
// CONNECT over raw TCP — stompit, NOT @stomp/stompjs (which is WebSocket-only).
const client: Client = await new Promise((resolve, reject) => {
connect({ host, port, connectHeaders: { "accept-version": "1.2",
"heart-beat": "10000,10000", login: "my-app" } },
(err, c) => (err ? reject(err) : resolve(c)));
});
const frame = client.send({ destination, "content-type": "text/plain" });
frame.write("hello from STOMP");
frame.end();
console.log(`sent 1 message to ${destination} (channel orders.new)`);
client.disconnect();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});using System.Text;
using Stomp.Net;
static string BrokerUri()
{
var url = Environment.GetEnvironmentVariable("KUBEMQ_STOMP_URL") ?? "tcp://localhost:61613";
var u = new Uri(url);
// Stomp.Net dispatches on the outer scheme: tcp:// or ssl://.
var transport = u.Scheme == "tls" ? "ssl" : "tcp";
return $"{transport}://{u.Host}:{(u.Port > 0 ? u.Port : 61613)}";
}
const string destination = "/queue/orders/new"; // Queues pattern, channel "orders.new"
// CONNECT: Stomp.Net performs the STOMP 1.2 handshake; no-auth default.
var factory = new ConnectionFactory(BrokerUri(), new StompConnectionSettings());
using var connection = factory.CreateConnection();
connection.Start();
using var session = connection.CreateSession(AcknowledgementMode.AutoAcknowledge);
using var producer = session.CreateProducer(session.GetQueue(destination));
var message = session.CreateBytesMessage(Encoding.UTF8.GetBytes("hello from STOMP"));
message.StompType = "text/plain";
producer.Send(message);
Console.WriteLine($"sent 1 message to {destination} (channel orders.new)");require "stomp"
require "uri"
uri = URI.parse(ENV.fetch("KUBEMQ_STOMP_URL", "tcp://localhost:61613"))
destination = "/queue/orders/new" # Queues pattern, channel "orders.new"
# CONNECT: accept-version 1.2, default heart-beat, no-auth.
client = Stomp::Client.new(
hosts: [{ host: uri.host, port: uri.port }],
connect_headers: { "accept-version" => "1.2", "heart-beat" => "10000,10000",
"login" => "my-app", "passcode" => "" },
)
client.publish(destination, "hello from STOMP", { "content-type" => "text/plain" })
puts "sent 1 message to #{destination} (channel orders.new)"
client.closeuse async_stomp::client::Connector;
use async_stomp::ToServer;
use futures::SinkExt;
#[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");
let destination = "/queue/orders/new"; // Queues pattern, channel "orders.new"
// CONNECT over raw TCP; async-stomp negotiates STOMP 1.2.
let mut conn = Connector::builder()
.server(host_port)
.login("my-app".to_string())
.passcode(String::new())
.connect()
.await?;
conn.send(ToServer::Send {
destination: destination.to_string(),
transaction: None,
headers: Some(vec![("content-type".to_string(), "text/plain".to_string())]),
body: Some(b"hello from STOMP".to_vec()),
})
.await?;
println!("sent 1 message to {destination} (channel orders.new)");
conn.send(ToServer::Disconnect { receipt: None }).await?;
Ok(())
}Supported languages
The connector speaks standard STOMP over raw TCP, so any conformant native STOMP client works.
The examples pin one client per language — there is no KubeMQ SDK, no proto bindings, and no
published package. Only go-stomp/v3 and stomp.py are proven by kubemq-server integration
tests; the others are wire-compatible.
| Language | Client library | Notes |
|---|---|---|
| Go | github.com/go-stomp/stomp/v3 | The connector's reference client. |
| Python | stomp.py | Listener-based; install via uv. |
| Java | Spring ReactorNettyTcpStompClient (spring-messaging) | Raw-TCP STOMP over Reactor Netty. |
| JavaScript / TypeScript | stompit | Raw TCP — not @stomp/stompjs (WebSocket-only). |
| C# / .NET | Stomp.Net | NMS-style API over STOMP 1.2. |
| Ruby | stomp gem | Native STOMP 1.0/1.1/1.2 client. |
| Rust | async-stomp | async/await on Tokio. |
@stomp/stompjs is WebSocket-only and cannot drive the STOMP connector. The connector
listens on raw TCP (61613/61614) with no WebSocket upgrade, so the JavaScript/TypeScript
examples use stompit (raw TCP). See
Connectivity and security.
Next steps
Getting started
Connect, send, and subscribe end-to-end through the STOMP connector in minutes.
Configuration
The 11 connector settings, the CONNECTORS_STOMP_ENABLE disable var, ports, and validation.
Events
Fan-out pub/sub over STOMP — wildcard subscriptions and the slash→dot channel mapping.
Destination grammar
The full destination grammar, prefix→pattern table, aliases, and wildcard rules.
Was this page helpful?