# Migrating from STOMP (/connectors/stomp/scenarios/migration-from-stomp)



Migrate a STOMP client application onto KubeMQ's native STOMP connector. The connector implements
STOMP 1.0 / 1.1 / 1.2 over plain TCP and TLS — no broker middleware is interposed, so your STOMP
client connects directly to KubeMQ. This is a **drop-in, endpoint-only** migration: change the host
and port, keep the same STOMP client library and application code.

The guide is **broker-agnostic** — it applies to any STOMP client driving any STOMP broker today:
`stomp.py`, `@stomp/stompjs`, `go-stomp/stomp/v3`, the Ruby `stomp` gem, `Stomp.Net`, or a Spring
`broker-relay` front-end. Only the library API differs; the wire protocol and the migration steps are
identical.

## Overview [#overview]

The KubeMQ STOMP connector is a faithful STOMP 1.0 / 1.1 / 1.2 raw-TCP endpoint. The code examples on
this page target **`stomp.py` 8.x** (Python) as the canonical client; other clients connect identically.

| Item             | Value                                                                                     |
| ---------------- | ----------------------------------------------------------------------------------------- |
| Plain TCP port   | 61613 (`Connectors.Stomp.Port`, default `"61613"`)                                        |
| TLS port         | 61614 (`Connectors.Stomp.TlsPort`, default `"61614"`; active only when `Security` ≠ none) |
| STOMP versions   | 1.0, 1.1, 1.2 (negotiated; highest common wins)                                           |
| Default pattern  | `events` (configurable via `Connectors.Stomp.DefaultPattern`)                             |
| Canonical client | `stomp.py` 8.x (Python)                                                                   |
| Enable env var   | `CONNECTORS_STOMP_ENABLE=true`                                                            |

The connector is **opt-in** — `Connectors.Stomp.Enable` defaults to `false`, so a stock kubemq-server
does **not** bind the STOMP listener until you turn it on:

<RunKubeMQ ports="[61613, 61614, 50000]" env="{ CONNECTORS_STOMP_ENABLE: 'true' }" />

<Callout type="warn">
  **The 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.
</Callout>

For the full wire-protocol contract (heartbeats, receipts, header/tag mapping, ack token internals,
error table, metrics catalog) see [Capabilities](/connectors/stomp/reference/capabilities),
[Destination grammar](/connectors/stomp/reference/destination-grammar), and
[Error frames](/connectors/stomp/reference/error-frames). For configuration fields and environment
variables see the [configuration reference](/configure/reference/connectors#stomp).

## Compatibility Matrix [#compatibility-matrix]

This is the STOMP column of the cross-protocol compatibility matrix in the
[Migration hub](/connectors/how-to/migration). It is self-contained — read it as the at-a-glance answer to
"what migrates?"

| Dimension                              | STOMP on KubeMQ                                                      |
| -------------------------------------- | -------------------------------------------------------------------- |
| **Drop-in level**                      | endpoint-only                                                        |
| **Point-to-point queues**              | ✅ `/queue/*` → Queues pattern                                        |
| **Pub/sub (non-durable)**              | ✅ `/topic/*` → Events pattern                                        |
| **Durable / persistent subscriptions** | ✅ via `/topic-store/*` + `start-from` replay headers                 |
| **Request / reply (RPC)**              | ✅ reply-to                                                           |
| **Ordering guarantee**                 | ⚠️ node-local                                                        |
| **Transactions**                       | ❌ rejected (BEGIN/COMMIT/ABORT → ERROR `transactions not supported`) |
| **Dead-letter / redrive**              | ❌ no client-settable DLQ; see footnote ¹                             |
| **Selectors / filtering / wildcards**  | ❌ no selectors                                                       |
| **Auth model**                         | JWT (CONNECT)                                                        |
| **TLS / mTLS**                         | ✅ 61614 when `Security` configured                                   |
| **Top unsupported**                    | transactions; selectors                                              |

> ¹ &#x2A;*No client-settable DLQ over STOMP.** The STOMP connector never sets `MaxReceiveQueue` on published
> messages, so a poison message that exceeds `MaxReceiveCount` is silently dropped by the broker — it is
> not delivered to any consumable dead-letter address. If you need client-facing dead-letter behaviour,
> use the RabbitMQ (DLX) or AWS (redrive) path instead.

## Connection / Endpoint Migration [#connection--endpoint-migration]

The only change required is the host and port. The STOMP protocol remains identical.

| Setting    | Source broker                      | KubeMQ                                                                                |
| ---------- | ---------------------------------- | ------------------------------------------------------------------------------------- |
| Host       | `stomp-host` (your current broker) | `kubemq-host`                                                                         |
| Plain port | 61613 (STOMP default)              | 61613 (same)                                                                          |
| TLS port   | 61614                              | 61614                                                                                 |
| Login      | broker-specific username           | any string (audit-only when auth disabled); or the `ClientID` value when auth enabled |
| Passcode   | broker password                    | KubeMQ JWT when `Authentication.Enable = true`; any value when auth disabled          |

### stomp.py 8.x — before [#stomppy-8x--before]

```python
import stomp

conn = stomp.Connection([("stomp-host", 61613)])
conn.connect("user", "password", wait=True)
```

### stomp.py 8.x — after (KubeMQ) [#stomppy-8x--after-kubemq]

```python
import stomp

conn = stomp.Connection([("kubemq-host", 61613)])
conn.connect("user", jwt_token, wait=True)   # jwt_token ignored when auth disabled
```

For TLS, use `stomp.Connection([("kubemq-host", 61614)], use_ssl=True)`.

## Concept & Destination Mapping [#concept--destination-mapping]

The STOMP `destination` header determines both the KubeMQ messaging **pattern** and the KubeMQ **channel**
name. Normalization strips one leading `/`, splits on `/`, joins remaining segments with `.`.

### Prefix table [#prefix-table]

| STOMP destination prefix | Aliases          | KubeMQ pattern         | Channel name                 |
| ------------------------ | ---------------- | ---------------------- | ---------------------------- |
| `/queue/NAME`            | `/queues/NAME`   | Queues                 | `NAME` (`.`-joined segments) |
| `/topic/NAME`            | `/events/NAME`   | Events                 | `NAME`                       |
| `/topic-store/NAME`      | `/store/NAME`    | Events Store           | `NAME`                       |
| `/command/NAME`          | `/commands/NAME` | Commands (RPC)         | `NAME`                       |
| `/query/NAME`            | `/queries/NAME`  | Queries (RPC)          | `NAME`                       |
| `/reply/ID`              | —                | connection-local reply | not routed to any channel    |

A destination whose first segment does not match any prefix maps to `Connectors.Stomp.DefaultPattern`
(default `events`). Setting `DefaultPattern = none` rejects those destinations with ERROR
`invalid destination`.

### Destination examples [#destination-examples]

| STOMP destination           | KubeMQ pattern | KubeMQ channel     |
| --------------------------- | -------------- | ------------------ |
| `/queue/orders`             | Queues         | `orders`           |
| `/queue/orders/new`         | Queues         | `orders.new`       |
| `/topic/orders.created`     | Events         | `orders.created`   |
| `/topic-store/audit-log`    | Events Store   | `audit-log`        |
| `/command/payments/process` | Commands       | `payments.process` |
| `/query/inventory/check`    | Queries        | `inventory.check`  |

### Wildcards [#wildcards]

The broker's native wildcards — `*` (single segment) and `>` (tail) — are supported **on SUBSCRIBE,
Events pattern only** (`/topic/*` or `/topic/orders.>`). These are the STOMP destination grammar's own
wildcards; there is **no** MQTT-style `+` / `#`. Wildcards on SEND, on any other pattern, or a misplaced
`>` are rejected with ERROR `invalid destination`.

### Durable subscriptions → Events Store + replay headers [#durable-subscriptions--events-store--replay-headers]

STOMP has no built-in durable-subscription mechanism. In KubeMQ the equivalent is subscribing to a
`/topic-store/` destination with a `start-from` replay header:

| `start-from` value | Meaning                                                               |
| ------------------ | --------------------------------------------------------------------- |
| `new` (default)    | Deliver only messages published after this subscription               |
| `first`            | Replay from the very first stored message                             |
| `last`             | Start from the most recently stored message                           |
| `sequence`         | Start at a specific sequence number (supply `start-value`)            |
| `time`             | Start at an RFC 3339 or unix-seconds timestamp (supply `start-value`) |
| `time-delta`       | Start N seconds before now (supply `start-value` in seconds)          |

Example: subscribe from the beginning of an Events Store channel:

```python
conn.subscribe(
    destination="/topic-store/audit-log",
    id="sub-1",
    ack="client-individual",
    headers={"start-from": "first"},
)
```

### Ack modes [#ack-modes]

Set per subscription via the `ack` header on SUBSCRIBE.

| `ack` value         | Semantics                                                                      | Applies to |
| ------------------- | ------------------------------------------------------------------------------ | ---------- |
| `auto` (default)    | Broker auto-acks on enqueue success; NAcks on enqueue failure (requeues)       | Queues     |
| `client-individual` | Each delivery tracked; ACK/NACK frame releases that one message                | Queues     |
| `client`            | Cumulative: ACK/NACK of delivery N acks/nacks all pending with order-index ≤ N | Queues     |

ACK frames on Events or Events Store subscriptions are accepted as a no-op (those patterns do not track
per-message delivery state).

## Canonical Client Example (stomp.py 8.x) [#canonical-client-example-stomppy-8x]

This example uses the following `stomp.py` 8.x API symbols: `stomp.Connection`, `conn.set_listener`,
`conn.connect`, `conn.send`, `conn.subscribe`, `conn.ack`, `conn.disconnect`,
`stomp.ConnectionListener.on_message`.

### Publish to a queue [#publish-to-a-queue]

```python
import stomp

KUBEMQ_HOST = "kubemq-host"
KUBEMQ_PORT = 61613
JWT_TOKEN = "..."            # omit or use any string when auth is disabled

conn = stomp.Connection([(KUBEMQ_HOST, KUBEMQ_PORT)])
conn.connect("myapp", JWT_TOKEN, wait=True)

conn.send(
    destination="/queue/orders",
    body="order payload",
    headers={"content-type": "text/plain"},
)

conn.disconnect()
```

### Subscribe and consume from a queue (client-individual ack) [#subscribe-and-consume-from-a-queue-client-individual-ack]

```python
import stomp

KUBEMQ_HOST = "kubemq-host"
KUBEMQ_PORT = 61613
JWT_TOKEN = "..."

class QueueListener(stomp.ConnectionListener):
    def __init__(self, conn):
        self._conn = conn

    def on_message(self, frame):
        print("received:", frame.body)
        # Acknowledge the individual message
        self._conn.ack(frame.headers["ack"])

    def on_error(self, frame):
        print("error:", frame.headers.get("message"))

conn = stomp.Connection([(KUBEMQ_HOST, KUBEMQ_PORT)])
conn.set_listener("", QueueListener(conn))
conn.connect("myapp", JWT_TOKEN, wait=True)

conn.subscribe(
    destination="/queue/orders",
    id="sub-orders",
    ack="client-individual",
)

input("Press Enter to stop...\n")
conn.disconnect()
```

### Pub/sub via Events [#pubsub-via-events]

```python
import stomp, threading

KUBEMQ_HOST = "kubemq-host"
KUBEMQ_PORT = 61613

received = threading.Event()

class EventListener(stomp.ConnectionListener):
    def on_message(self, frame):
        print("event:", frame.body)
        received.set()

conn = stomp.Connection([(KUBEMQ_HOST, KUBEMQ_PORT)])
conn.set_listener("", EventListener())
conn.connect(wait=True)

conn.subscribe(destination="/topic/orders.created", id="sub-1", ack="auto")
conn.send(destination="/topic/orders.created", body="event payload")

received.wait(timeout=5)
conn.disconnect()
```

### RPC (Commands) with reply-to [#rpc-commands-with-reply-to]

The reply subscription **must be created before** sending the RPC request. See
[What Does NOT Migrate / Deviations](#what-does-not-migrate--deviations) for details.

```python
import stomp, threading, uuid

KUBEMQ_HOST = "kubemq-host"
KUBEMQ_PORT = 61613
JWT_TOKEN = "..."

reply_event = threading.Event()
reply_body = None

class RpcListener(stomp.ConnectionListener):
    def on_message(self, frame):
        global reply_body
        reply_body = frame.body
        reply_event.set()

reply_dest = f"/reply/{uuid.uuid4()}"

conn = stomp.Connection([(KUBEMQ_HOST, KUBEMQ_PORT)])
conn.set_listener("", RpcListener())
conn.connect("myapp", JWT_TOKEN, wait=True)

# Step 1: subscribe to the reply destination BEFORE sending the request
conn.subscribe(destination=reply_dest, id="reply-sub", ack="auto")

# Step 2: send the RPC request with reply-to and correlation-id headers
conn.send(
    destination="/command/payments/process",
    body="payment request",
    headers={
        "reply-to": reply_dest,
        "correlation-id": str(uuid.uuid4()),
        "timeout": "5000",       # ms; capped at RpcTimeoutSeconds * 1000
    },
)

reply_event.wait(timeout=10)
print("reply:", reply_body)
conn.disconnect()
```

## Security [#security]

**Authentication (connect-time only).** When `Authentication.Enable = true`, the CONNECT frame `passcode`
header must carry a valid KubeMQ JWT. The `login` header is recorded for audit purposes. Auth failure
results in ERROR `authentication failed` + connection close. When authentication is disabled, any
credentials are accepted — no auth is performed.

Token expiry does not terminate an established connection (connect-time-only auth, consistent with the
AMQP and MQTT connectors).

**Authorization.** When `Authorization.Enable = true`, SEND enforces the `Write` permission and SUBSCRIBE
enforces `Read` on `ControlRecord{Resource: <pattern>, ClientID, Channel}`. Casbin policies must cover
`stomp-*` client IDs for the mapped channels. Reply (`/reply/...`) destinations are authorization-exempt
(connection-local).

**No-auth exposure note.** When `Authentication.Enable = false` (the server default), the STOMP listener
accepts any `login` / `passcode`. If the server is reachable from untrusted networks, enable authentication
or restrict access with a firewall.

**TLS.** Port 61614 is active only when `Connectors.Stomp.TlsPort` is set and `Security` is configured
(mode ≠ none). Plain TCP on 61613 carries no transport encryption.

**Enabling the connector.**

```toml title="config.toml"
[Connectors.Stomp]
  Enable = true
  Port   = "61613"
  TlsPort = "61614"
```

For the full auth and Casbin authorization setup, see
[Authentication & Security](/connectors/reference/auth-and-security).

## What Does NOT Migrate / Deviations [#what-does-not-migrate--deviations]

### Hard failures (client will receive ERROR + connection close) [#hard-failures-client-will-receive-error--connection-close]

| Feature                                                                             | Behaviour on KubeMQ                                                                                                               |
| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **STOMP transactions** — BEGIN/COMMIT/ABORT frames, or any `transaction` header     | ERROR `transactions not supported` + close. Remove all `transaction` usage before migrating.                                      |
| **Message selectors** — `selector` header on SUBSCRIBE                              | ERROR `selectors not supported` + close. Remove selector headers. KubeMQ does not support broker-side SQL92 filtering over STOMP. |
| **Subscribing to RPC destinations** — SUBSCRIBE to `/command/*` or `/query/*`       | ERROR `cannot subscribe to RPC destinations`. Use the reply-to flow instead.                                                      |
| **Invalid wildcards** — wildcards on SEND, on non-Events patterns, or misplaced `>` | ERROR `invalid destination` (see the [Wildcards](#wildcards) subsection).                                                         |

### Behavioural deviations [#behavioural-deviations]

| Feature                                            | Deviation                                                                                                                                                                                                                                                                                                     |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **RPC reply-to ordering**                          | The reply subscription (`/reply/ID`) **must exist on the same connection before** the SEND that carries `reply-to`. If the reply subscription is not already active, the SEND returns ERROR `reply-to subscription required` + close. This differs from brokers that buffer replies for a later subscription. |
| **No client-settable DLQ**                         | See footnote ¹ in the [Compatibility Matrix](#compatibility-matrix). Poison messages exceeding `MaxReceiveCount` are silently dropped — there is no consumable dead-letter address over STOMP.                                                                                                                |
| **Durable subscriptions replaced by Events Store** | The `durable-subscription-name` / `activemq.subscriptionName` SUBSCRIBE header is not recognized. Use `/topic-store/NAME` with `start-from` headers for persistent, replayable subscriptions.                                                                                                                 |
| **Ordering is node-local**                         | Message ordering within a channel is maintained on the receiving node; no cross-node total ordering is guaranteed in a clustered deployment.                                                                                                                                                                  |
| **STOMP-over-WebSocket**                           | Not supported in V1. Only plain TCP (61613) and TLS (61614) listeners exist. Spring STOMP-over-WebSocket front-ends require a V2 connector upgrade.                                                                                                                                                           |
| **ActiveMQ `STOMP 1.0` selector behaviour**        | KubeMQ rejects selectors loudly (ERROR + close) rather than ignoring them. Applications that set `selector` on any SUBSCRIBE must remove the header.                                                                                                                                                          |
| **`/temp-queue/`, `/temp-topic/` destinations**    | Not supported. Use a `/reply/{id}` connection-local destination for the request/reply use case.                                                                                                                                                                                                               |
| **Spring `/app/` destination conventions**         | Not a broker concept — not supported.                                                                                                                                                                                                                                                                         |

## Verification Smoke Test [#verification-smoke-test]

The recipe below uses the `stomp.py` 8.x snippets from
[Canonical Client Example](#canonical-client-example-stomppy-8x) and confirms that a message published to
a queue arrives at a subscriber.

### Steps [#steps]

1. Start KubeMQ with the STOMP connector enabled:

   <RunKubeMQ ports="[61613, 61614, 50000]" env="{ CONNECTORS_STOMP_ENABLE: 'true' }" />

2. In a terminal, run the consumer script (subscribes to `/queue/smoke-test`):

   ```python
   # consumer.py — stomp.py 8.x
   import stomp, time

   class Listener(stomp.ConnectionListener):
       def __init__(self, conn):
           self._conn = conn

       def on_message(self, frame):
           print("RECEIVED:", frame.body)
           self._conn.ack(frame.headers["ack"])

   conn = stomp.Connection([("localhost", 61613)])
   conn.set_listener("", Listener(conn))
   conn.connect(wait=True)
   conn.subscribe(destination="/queue/smoke-test", id="s1", ack="client-individual")

   time.sleep(10)    # wait for a message
   conn.disconnect()
   ```

3. In a second terminal, publish one message:

   ```python
   # producer.py — stomp.py 8.x
   import stomp

   conn = stomp.Connection([("localhost", 61613)])
   conn.connect(wait=True)
   conn.send(destination="/queue/smoke-test", body="hello from stomp")
   conn.disconnect()
   print("sent")
   ```

4. Confirm the consumer terminal prints `RECEIVED: hello from stomp`.

5. To verify Events Store replay, repeat with `/topic-store/smoke-test` and
   `headers={"start-from": "first"}` on the subscriber. The message should be delivered even if the
   subscriber connects after the publisher disconnects.

## See Also [#see-also]

<Cards>
  <Card title="Migration hub" href="/connectors/how-to/migration" description="The connector map, the cross-protocol comparison matrix, and guides for every messaging ecosystem." />

  <Card title="Getting Started" href="/connectors/stomp/tutorials/getting-started" description="Connect a stock STOMP client and run a publish-and-subscribe round-trip in minutes." />

  <Card title="Architecture" href="/connectors/stomp/concepts/architecture" description="The embedded STOMP server, the frame codec, version negotiation, and the destination router." />

  <Card title="Destination Grammar" href="/connectors/stomp/reference/destination-grammar" description="The destination grammar you are mapping your existing destinations onto." />

  <Card title="Capabilities" href="/connectors/stomp/reference/capabilities" description="The full supported / out-of-scope matrix, ack token resolution, and the wire-protocol contract." />

  <Card title="Configuration Reference" href="/configure/reference/connectors#stomp" description="All STOMP connector settings and environment variables, including the CONNECTORS_STOMP_ENABLE spelling." />

  <Card title="Migrating from ActiveMQ" href="/connectors/how-to/migration/from-activemq" description="Route ActiveMQ onto KubeMQ by client type — JMS, STOMP, and MQTT — when STOMP is one of several access paths." />
</Cards>
