KubeMQ
ConnectorsHow-to guidesMigrate from another broker

Migrating from JMS

Swap your JMS ConnectionFactory to Apache Qpid JMS over KubeMQ's AMQP 1.0 connector: quick start, destination mapping, a runnable test, XA gaps.

JMS (Jakarta / Java Message Service) is a Java API specification, not a wire protocol. Any conforming JMS provider can be swapped for another without touching application code — your program calls Session.createProducer, Session.createConsumer, and so on regardless of the underlying transport. The migration path is therefore a ConnectionFactory swap: replace your current provider's factory with Apache Qpid JMS, which speaks AMQP 1.0 over the wire, and point it at KubeMQ's AMQP 1.0 connector. The JMS calls you already wrote stay exactly as they are.

This guide covers the connection-factory migration, the concept-to-pattern mapping, a self-contained Java snippet, the security posture, and the documented gaps. For the AMQP 1.0 wire contract behind Qpid JMS — frame-level semantics, link options, message translation, selector grammar, and settlement rules — see the Migrating from AMQP 1.0 guide and the AMQP 1.0 connector capabilities reference.

Overview

KubeMQ's AMQP 1.0 connector is the substrate. Qpid JMS connects to it over AMQP 1.0 on port 5672 (plain / SASL) or 5671 (TLS), sharing the listener with AMQP 0.9.1.

ConnectorAMQP 1.0
Ports5672 (plain / SASL), 5671 (TLS)
Canonical clientApache Qpid JMS 2.x (jakarta namespace) — org.apache.qpid:qpid-jms-client:2.x
Enable defaultOpt-in — disabled by default. Set CONNECTORS_AMQP10_ENABLE=true (or connectors.amqp10.enable = true in TOML) to turn it on. Port 5672 is already bound by the AMQP 0-9-1 connector (on by default); without AMQP 1.0 enabled a client connects and is closed during protocol negotiation; on Kubernetes, set spec.amqp10.enabled: true

Qpid JMS 2.x vs 1.x. The jakarta-namespace release (org.apache.qpid:qpid-jms-client:2.x, artifact classifier jakarta) requires jakarta.jms:jakarta.jms-api:3.x. If your codebase still targets the javax.jms namespace (JMS 2.0), use the 1.x line (org.apache.qpid:qpid-jms-client:1.x) instead. The Qpid URI format, the AMQP addressing, and all JNDI properties are identical between the two lines — only the JMS API namespace differs.

Quick start: repoint one client

Try the switch on your own machine before you plan the migration.

The Docker tab points one client at the KubeMQ you started with Try KubeMQ, turning its connector on where needed and keeping your evaluation or license and your data. The Kubernetes tab works on a cluster you installed with Install on Kubernetes. Use it to try the switch, not for production; see Plans compared for staging and production licenses. If your current broker runs on this machine, stop it first, because two programs cannot publish the same port. The kmq command runs natively on Windows; the other commands are bash, so on Windows run them in WSL (Windows Subsystem for Linux).

You started KubeMQ with Try KubeMQ or Install with Docker.

Turn the AMQP 1.0 connector on

The connector is off by default. This command turns it on and publishes port 5672 on the same container and volume. If you started KubeMQ with the Docker command, skip it and use Without kmq below.

Terminal
kmq deploy update --installation kubemq --connector amqp10 --deadline 10m

Check that installation_id is the same as before and data_retained is true. You should see:

Output
{
  "installation_id": "…",
  "state": "…",
  "target": "…",
  "data_retained": true,
  "next_action": "…"
}

Point your client at KubeMQ

Before
// Any other JMS provider, for example ActiveMQ
ConnectionFactory cf = new ActiveMQConnectionFactory("tcp://old-broker:61616");
After
// Qpid JMS over KubeMQ's AMQP 1.0 connector
ConnectionFactory cf = new JmsConnectionFactory("amqp://localhost:5672");

Send and receive one message

You need Java and JBang. Save the test and run it; JBang downloads Qpid JMS on the first run. The test uses the javax.jms line (Qpid JMS 1.x); for jakarta.jms, use Qpid JMS 2.x and change the imports.

SmokeTest.java
//DEPS org.apache.qpid:qpid-jms-client:1.16.0

import javax.jms.Connection;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.Session;

import org.apache.qpid.jms.JmsConnectionFactory;

public class SmokeTest {
    public static void main(String[] args) throws Exception {
        // The only KubeMQ-specific line: Qpid JMS pointed at the AMQP 1.0 connector.
        JmsConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:5672");
        try (Connection connection = factory.createConnection()) {
            connection.start();
            Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            Queue queue = session.createQueue("queues/smoke-test");
            session.createProducer(queue).send(session.createTextMessage("hello kubemq"));
            Message received = session.createConsumer(queue).receive(10_000);
            if (received == null) {
                throw new IllegalStateException("no message within 10 seconds");
            }
            System.out.println("received: " + received.getBody(String.class));
        }
    }
}
Terminal
jbang SmokeTest.java

You should see, as the last line:

Output
received: hello kubemq

To turn the connector off again, run the same kmq deploy update command without --connector. Your messages and your evaluation or license stay.

You installed KubeMQ with Install on Kubernetes.

Turn the connector on

Put the connector's key in cluster-values.yaml. If Helm installed your cluster, add it to the file you created on Install on Kubernetes; if kmq installed it, create the file with only this key.

cluster-values.yaml (add)
amqp10:
  enabled: true

If kmq installed your cluster, apply the file with kmq:

Terminal
kmq deploy update --installation messaging --kube-context YOUR_KUBE_CONTEXT --values cluster-values.yaml --deadline 10m

Replace:

  • YOUR_KUBE_CONTEXT — your cluster's kubectl context; kubectl config get-contexts lists them.

Check that data_retained is true. You should see:

Output
{
  "installation_id": "…",
  "state": "…",
  "target": "…",
  "data_retained": true,
  "next_action": "…"
}

If Helm installed your cluster, apply the file with the command in Apply a settings change.

Either way, check that the connector's Service exists:

Terminal
kubectl --context YOUR_KUBE_CONTEXT -n kubemq get svc messaging-amqp10

Check that the row lists 5672/TCP. You should see:

Output
NAME     TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE
messaging-amqp10    ClusterIP   …            <none>        5672/TCP,…     …

Reach the connector from your machine

Keep this running in a second terminal:

Terminal 2
kubectl --context YOUR_KUBE_CONTEXT -n kubemq port-forward svc/messaging-amqp10 5672:5672

You should see:

Output
Forwarding from 127.0.0.1:5672 -> 5672
Forwarding from [::1]:5672 -> 5672

Send and receive one message

Run steps 2 and 3 of the Docker tab unchanged. The port-forward serves the connector at the same localhost:5672 address.

Before you move production traffic

  • Read What does not migrate before you commit.
  • Plan how traffic moves and how you roll back: run both brokers, move one queue or topic at a time, and keep your JMS provider until you have verified the move.
  • Install KubeMQ for real: Choose your path.

Compatibility Matrix

This is the JMS column of the cross-protocol matrix on Migrate from another broker, restated here so this guide stands on its own.

DimensionJMS (Qpid JMS over AMQP 1.0)Notes
Drop-in levelclient-swapSwap the ConnectionFactory; JMS API code unchanged
Point-to-point queues✅Queue → queues/<channel>
Pub/sub (non-durable)✅Topic → events/<channel>
Durable subscriptions✅¹Topic → events-store/<channel> (persistence-backed)
Request / reply (RPC)✅QueueRequestor / TemporaryQueue → Commands / Queries via dynamic nodes
Ordering guarantee⚠️ node-localQueue ordering within a single node; no cluster-wide guarantee
Transactions❌SESSION_TRANSACTED and XA are not supported
Dead-letter / redrive❌ no client DLQ²Poison messages are dropped after MaxReceiveCount; no consumable dead-letter address
Selectors / filtering✅ selectors (SQL92 subset)createConsumer(dest, selector) works on Events / Events Store
Auth modelPLAIN (JWT) / EXTERNALSASL PLAIN with a KubeMQ JWT as password; EXTERNAL via mTLS
TLS / mTLS✅ 5671Active when the server Security block is configured
Top unsupportedJMS transactions / XA; node-local unsubscribe()See What Does NOT Migrate

¹ Durable subscription is backed by the persistence engine. session.unsubscribe() is node-local — see What Does NOT Migrate for the full explanation.

² No client-settable DLQ — see What Does NOT Migrate for the full explanation.

Connection / Endpoint Migration

As shown in the Compatibility Matrix (Drop-in level: client-swap), the only required change is the ConnectionFactory URL and its class name. The JMS API calls (createSession, createProducer, createConsumer, and so on) remain unchanged.

Using JNDI properties

# jndi.properties — before (ActiveMQ / Artemis example)
# java.naming.factory.initial=org.apache.activemq.artemis.jndi.ActiveMQInitialContextFactory
# connectionFactory.kubemq=tcp://old-broker:61616

# jndi.properties — after (Qpid JMS over KubeMQ AMQP 1.0)
java.naming.factory.initial=org.apache.qpid.jms.jndi.JmsInitialContextFactory
connectionfactory.kubemq=amqp://kubemq.example.com:5672

# Optional TLS endpoint
# connectionfactory.kubemq=amqps://kubemq.example.com:5671

# Declare destinations (prefix drives pattern selection)
queue.ordersQueue=queues/orders
topic.ordersTopic=events/orders
topic.auditTopic=events-store/audit

Direct instantiation (no JNDI)

import org.apache.qpid.jms.JmsConnectionFactory;

// Before (example from any other provider):
// ConnectionFactory cf = new ActiveMQConnectionFactory("tcp://old-broker:61616");

// After (Qpid JMS):
ConnectionFactory cf = new JmsConnectionFactory("amqp://kubemq.example.com:5672");

For TLS, use "amqps://kubemq.example.com:5671".

Maven / Gradle dependency

The examples on this page use qpid-jms-client 2.6.0 (jakarta) or 1.16.0 (javax); newer releases in the same line are expected to work.

<!-- Maven — jakarta namespace (JMS 3.x) -->
<dependency>
  <groupId>org.apache.qpid</groupId>
  <artifactId>qpid-jms-client</artifactId>
  <version>2.6.0</version>
  <classifier>jakarta</classifier>
</dependency>

<!-- javax namespace (JMS 2.0), if still on javax.jms -->
<!-- <artifactId>qpid-jms-client</artifactId><version>1.16.0</version> -->
// Gradle — jakarta namespace (JMS 3.x)
implementation 'org.apache.qpid:qpid-jms-client:2.6.0:jakarta'

// javax namespace (JMS 2.0), if still on javax.jms
// implementation 'org.apache.qpid:qpid-jms-client:1.16.0'

Concept & Destination Mapping

Qpid JMS resolves destination names against KubeMQ by the address prefix — the prefix is stripped and the remainder becomes the channel name. JMS Queue / Topic objects carry the full address, not just the channel name.

JMS conceptQpid JMS addressKubeMQ patternChannel name
Queue (point-to-point)queues/ordersQueuesorders
Topic (non-durable pub/sub)events/notificationsEventsnotifications
Durable Topic subscriptionevents-store/auditEvents Storeaudit
QueueRequestor / TemporaryQueue replycommands/status + dynamic reply nodeCommandsstatus
Virtual Topic (ActiveMQ Classic)events-store/<channel> + group propertyEvents Store—

JMS 2.0 shared subscriptions. createSharedConsumer does not automatically enable consumer-group load balancing. The AMQP 1.0 connector activates group load-balancing through a wire-level AMQP link property set on the ATTACH frame — which is not expressible through the standard JMS API. The createSharedConsumer(dest, groupName) call by itself does not wire the group; to load-balance Events across a group with Qpid JMS, the link property must be set at the wire level.

Bare address (no prefix). Qpid JMS advertises a JMS capability hint (queue or topic) in the AMQP ATTACH. The connector uses this to resolve Queue("orders") → Queues and Topic("orders") → Events automatically, without requiring the prefix. If you use addresses that lack the prefix and the capability hint is absent, the connector falls back to the configured default pattern (queues). Prefer the explicit prefix so resolution is never ambiguous.

Selectors. Pass the selector expression as the second argument to createConsumer or createDurableConsumer. The connector evaluates a SQL92 subset (comparisons, AND / OR / NOT, BETWEEN, IN, LIKE, IS NULL) against application properties and JMS headers (JMSPriority, JMSType, JMSCorrelationID, JMSMessageID, JMSTimestamp). Selectors are supported on Events and Events Store links only; a selector on a Queue (queues/) link fails the ATTACH with amqp:not-implemented.

Canonical Client Example

Client: Apache Qpid JMS 2.x (jakarta). API symbols: JmsConnectionFactory, Connection.createSession, Session.AUTO_ACKNOWLEDGE, Session.createQueue, Session.createTopic, Session.createProducer, Session.createConsumer, Session.createDurableConsumer, Session.createTemporaryQueue, MessageProducer.send, MessageConsumer.receive.

import jakarta.jms.*;
import org.apache.qpid.jms.JmsConnectionFactory;

public class KubeMQJmsExample {

    public static void main(String[] args) throws JMSException {

        // 1. ConnectionFactory — the only change from your previous provider
        ConnectionFactory cf = new JmsConnectionFactory(
            "amqp://kubemq.example.com:5672");

        // When authentication is enabled, pass the KubeMQ JWT as the password.
        // The username is recorded for audit only and does not affect identity.
        Connection conn = cf.createConnection("svc-orders", System.getenv("KUBEMQ_JWT"));
        conn.start();

        Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);

        // ── Point-to-point (Queues) ──────────────────────────────────────────
        Queue ordersQueue = session.createQueue("queues/orders");

        // Produce
        TextMessage msg = session.createTextMessage("order-payload");
        session.createProducer(ordersQueue).send(msg);

        // Consume (competing consumer — AUTO_ACKNOWLEDGE maps to AMQP ACCEPTED)
        TextMessage received = (TextMessage) session.createConsumer(ordersQueue).receive(5000);
        System.out.println("Received: " + received.getText());

        // ── Non-durable pub/sub (Events) ─────────────────────────────────────
        Topic eventsTopic = session.createTopic("events/notifications");
        session.createProducer(eventsTopic).send(
            session.createTextMessage("event-payload"));
        // Subscribers on events/notifications receive a copy (fan-out).

        // ── Durable subscription (Events Store) ──────────────────────────────
        Topic auditTopic = session.createTopic("events-store/audit");
        // Durable identity derived from (container-id, subscription-name) — survives reconnect.
        MessageConsumer durableConsumer =
            session.createDurableConsumer(auditTopic, "audit-sub");

        // ── Selector on Events Store ──────────────────────────────────────────
        // SQL92 subset evaluated in the connector before delivery.
        MessageConsumer filtered = session.createConsumer(
            session.createTopic("events-store/audit"),
            "JMSPriority > 4 AND region = 'EU'");

        // ── Request / reply (Commands) ────────────────────────────────────────
        // Use a TemporaryQueue as the reply destination; the connector backs it
        // with a dynamic AMQP node (/responses/<id>).
        Queue statusCmd = session.createQueue("commands/status");
        TemporaryQueue replyQ = session.createTemporaryQueue();
        Message req = session.createTextMessage("get-status");
        req.setJMSReplyTo(replyQ);
        req.setJMSCorrelationID("req-001");
        session.createProducer(statusCmd).send(req);

        Message reply = session.createConsumer(replyQ).receive(5000);
        System.out.println("Reply: " + ((TextMessage) reply).getText());

        conn.close();
    }
}

Acknowledge modes and AMQP settlement. AUTO_ACKNOWLEDGE → AMQP accepted (AckRange). CLIENT_ACKNOWLEDGE → deferred accepted on msg.acknowledge(). SESSION_TRANSACTED is not supported — see What Does NOT Migrate.

Security

Authentication

  • Opt-in connector. The AMQP 1.0 connector is disabled by default. Set CONNECTORS_AMQP10_ENABLE=true (or connectors.amqp10.enable = true in TOML) to start it. Port 5672 is open regardless, because the AMQP 0-9-1 connector sharing it is on by default — so an open port does not by itself mean AMQP 1.0 is enabled.

  • No auth (development). When authentication is disabled (the server default), the connector accepts SASL PLAIN with any credentials and also accepts SASL ANONYMOUS. If the server is reachable from untrusted networks, enable authentication or firewall ports 5672 / 5671.

  • With auth enabled. Pass a KubeMQ JWT as the SASL PLAIN password:

    Connection conn = cf.createConnection("any-username", kubemqJWT);

    The username is recorded for audit; the JWT's ClientID claim becomes the connection identity for authorization checks.

Authorization

With authorization enabled, the ClientID derived from the JWT is checked against the policy on the resolved channel at ATTACH time:

  • Producers (sender links) need Write on the channel.
  • Consumers (receiver links) need Read on the channel.

A denied ATTACH fails with amqp:unauthorized-access; the JMS client throws a JMSSecurityException.

TLS / mTLS

  • TLS (server auth). Change the connection URL to amqps://kubemq.example.com:5671. The Qpid JMS client performs the standard TLS handshake; trust the server certificate using a JVM truststore or the transport.trustStoreLocation / transport.trustStorePassword URI options:

    amqps://kubemq.example.com:5671?transport.trustStoreLocation=/path/to/truststore.jks
  • mTLS (client auth). When the server requests a client certificate, the connector offers SASL EXTERNAL; the certificate CN becomes the ClientID (no JWT needed):

    amqps://kubemq.example.com:5671?transport.keyStoreLocation=/path/to/keystore.jks&transport.keyStorePassword=secret

The TLS listener on port 5671 is active only when the server Security block is configured. See Auth & Security for the shared connector security model.

SASL mechanisms

MechanismWhen offeredCredential
PLAINalwayspassword = KubeMQ JWT (auth enabled) or anything (auth disabled)
ANONYMOUSauthentication disabledClientID = sanitized container-id
EXTERNALmTLS with verified client certClientID = certificate CN

What Does NOT Migrate / Deviations

Not supported

JMS featureStatusNotes
SESSION_TRANSACTED❌ not supportedJMS transacted sessions rely on an AMQP transaction coordinator (declare / discharge), which the connector does not implement. Fall back to AUTO_ACKNOWLEDGE or CLIENT_ACKNOWLEDGE with idempotent message design.
XA transactions❌ not supportedJMS XA (XASession, XAConnectionFactory) require two-phase AMQP transactions. Not available.
Client-settable DLQ❌ no client DLQWhen a queue message exceeds MaxReceiveCount (default 1024), it is silently dropped — not delivered to a dead-letter address. The AMQP 1.0 connector never routes poison messages to a consumable dead-letter channel. Design for idempotency and monitor kubemq_messages_dropped_total (CounterVec; label cause="dropped").
session.unsubscribe() (cluster-wide)⚠️ node-local onlyunsubscribe() removes the durable Events Store subscription registration on the node where it is called. If the durable subscription is active on another cluster node, that registration is not removed. Use the REST / dashboard API to delete durable subscriptions cluster-wide.
rcv-settle-mode=second❌ not supportedTwo-phase receiver settlement (exactly-once) is not supported. The model is at-least-once (queue / durable Events Store) or at-most-once (Events fire-hose).
AMQP-over-WebSocket❌ not supportedNo WebSocket binding; use raw TCP / TLS on 5672 / 5671.
Selectors on Queues❌ not supportedSelector expressions on queues/ links fail the ATTACH with amqp:not-implemented. Selectors work on Events and Events Store links.
JMS start-position from Qpid JMS❌ not expressibleQpid JMS exposes no API for the x-opt-kubemq-start link property — you cannot set an arbitrary Events-Store start position from JMS. Use a native durable consumer with new-only, or a native client (Go / .NET / Python / Rust / JS) for first / last / sequence: / time:.

Behavioral deviations

AreaBehavior
RPC reply shape differs by patternA JMS request / reply over a dynamic reply node behaves differently depending on the target pattern: a commands/ reply carries executed / error properties, while a queries/ reply carries body + metadata only. Read the reply accordingly — do not expect executed/error flags on a Queries response.
Pub/sub events at credit 0events/ subscribers are fire-hose: if the consumer grants no link credit, events are dropped (kubemq_amqp10_events_dropped_no_credit_total). Grant credit continuously with an async consumer.
Events Store stalled creditA per-link buffer (MaxUnsettledPerLink, default 1024) fronts the durable subscription. If it fills while credit stays at 0, the link is detached (amqp:resource-limit-exceeded) and the buffered window is dropped. A durable re-attach resumes after the dropped window — those messages are lost. Size MaxUnsettledPerLink accordingly.
Durable-subscription registry is node-localAttaching the same durable subscription (same container-id + subscription name) on two cluster nodes produces a clientID conflict in the persistence engine. One attachment should be active at a time per durable-subscription identity.
JMSXGroupID / group-sequenceRound-trips losslessly but provides no ordering or consumer-affinity guarantee in KubeMQ.
header.priorityRound-trips as the amqp10.priority tag but drives no priority scheduling.
released increments receive countReleasing a message (rollback / recover()) increments the receive count, which counts toward MaxReceiveCount. A strict reading of AMQP would not count a release as a delivery attempt.
modified{undeliverable-here=true}Treated as a delivery failure (NAckRange); there is no per-consumer exclusion, so the message may be redelivered to the same consumer.

What does migrate cleanly

  • JMS AUTO_ACKNOWLEDGE and CLIENT_ACKNOWLEDGE modes.
  • Queue (point-to-point), Topic (fan-out), and durable Topic subscriptions.
  • TextMessage, BytesMessage, MapMessage, and ObjectMessage body types (round-trip lossless for standard properties).
  • Selectors on Events / Events Store.
  • JMSReplyTo + JMSCorrelationID for request / reply over a TemporaryQueue.
  • JMSPriority, JMSTimestamp, JMSMessageID, JMSType, and JMSCorrelationID headers.

Verification Smoke Test

For the basic round trip, see the quick start; this section adds deeper checks.

  1. Check the dashboard — the KubeMQ dashboard AMQP 1.0 page should show one connection, one sender link, one receiver link, and one transfer in / out.
  2. Confirm zero dropped messages — kubemq_amqp10_events_dropped_no_credit_total and kubemq_amqp10_events_store_dropped_stalled_total should remain 0.

See Also

Was this page helpful?

On this page