Migrating from RabbitMQ
Point a RabbitMQ app at KubeMQ by changing only the AMQP 0-9-1 connection string — exchanges, DLX, confirms, and the connector deviations.
RabbitMQ applications using any standard AMQP 0-9-1 client library (pika, amqp091-go,
php-amqplib, Spring AMQP, amqplib for Node.js, …) can point at KubeMQ by changing only
the connection string — no code changes — for the supported feature set. KubeMQ speaks the
RabbitMQ wire dialect, so queue.declare, exchange.declare, publisher confirms, DLX, and
Direct Reply-To work as-is. This is an endpoint-only drop-in: same library, same code,
same protocol, with no KubeMQ SDK to adopt.
Overview
| Connector | KubeMQ RabbitMQ connector (AMQP 0-9-1 RabbitMQ dialect) |
| Ports | 5672 (AMQP plain) / 5671 (AMQPS/TLS) |
| Canonical client | pika 1.x (Python) |
| Drop-in level | Endpoint-only — change the connection string; keep all client library code unchanged for the supported feature set |
| Enable default | Opt-in — Connectors.Amqp.Enable = false by default. Set CONNECTORS_AMQP_ENABLE=true (or Enable = true in TOML) to open the listener |
The RabbitMQ connector is disabled by default — a stock kubemq-server does not bind the AMQP listener until you turn it on:
docker run -d \ --name kubemq \ -p 5672:5672 \ -p 5671:5671 \ -p 50000:50000 \ -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY \ -e CONNECTORS_AMQP_ENABLE=true \ europe-docker.pkg.dev/kubemq/images/kubemq:nextThe enable variable is CONNECTORS_AMQP_ENABLE — this is the AMQP 0-9-1 (RabbitMQ)
connector, distinct from the AMQP 1.0 connector's CONNECTORS_AMQP10_ENABLE. For Kubernetes,
set spec.amqp.enabled: true in the KubemqCluster CR.
Compatibility Matrix
The cells below are the RabbitMQ column of the migration hub's master cross-protocol matrix.
| Dimension | RabbitMQ on KubeMQ |
|---|---|
| Point-to-point queues | ✅ |
| Pub/sub (non-durable) | ✅ exchanges |
| Durable / persistent subscriptions | ✅ durable queues |
| Request / reply (RPC) | ✅ Direct Reply-To |
| Ordering guarantee | ✅ per-queue¹ |
| Transactions | ❌ (use publisher confirms) |
| Dead-letter / redrive | ✅ DLX + x-death² |
| Selectors / filtering / wildcards | ✅ topic/headers routing |
| Auth model | SASL PLAIN (JWT) |
| TLS / mTLS | ✅ 5671 |
| Top unsupported | See Hard rejections and Accepted-but-inert arguments below |
¹ Requeued messages re-enter at the queue tail, not near the head — see Requeue → tail.
² Dead-lettering fires on the rejected trigger only; the expired trigger does not exist.
Poison messages exceeding MaxReceiveCount are silently dropped — there is no consumable
dead-letter address for those over this protocol.
Connection / Endpoint Migration
Replace the RabbitMQ host and port with the KubeMQ host. The URI scheme, client library, and application code stay the same:
# Before (RabbitMQ)
amqp://user:password@rabbitmq.example.com:5672/
amqps://user:password@rabbitmq.example.com:5671/myvhost
# After (KubeMQ)
amqp://user:password@kubemq.example.com:5672/
amqps://user:password@kubemq.example.com:5671/myvhostKey differences to be aware of at connection time:
| Aspect | RabbitMQ | KubeMQ |
|---|---|---|
| SASL mechanism | PLAIN, AMQPLAIN, EXTERNAL, … | PLAIN only |
| Password | RabbitMQ user password | KubeMQ JWT when Authentication.Enable = true; any value when auth is disabled (username recorded for audit) |
| Vhost | Must be pre-created | Namespace-on-first-use — any charset-valid vhost is accepted; / maps to the configured DefaultVhost (default "default") |
| Client identity | Connection name (optional) | amqp-{connection_name|uuid8} — used for Casbin authorization; policies must cover amqp-* IDs |
| TLS | Per-listener config | Active when the server's Security block is configured; serves the same certificates as gRPC/REST |
| Queue storage | RabbitMQ queues | KubeMQ Queue channels amqp.{vhost}.{queue} |
Concept & Destination Mapping
AMQP's exchange/binding model is implemented connector-side as virtual routing. Only queues hold messages; exchanges and bindings are metadata resolved at publish time.
| RabbitMQ concept | KubeMQ pattern | Channel / address |
|---|---|---|
| Queue | Queues | amqp.{vhost}.{queue} |
| Exchange (direct / fanout / topic / headers) | Virtual routing to queues | — |
| Binding | Routing rule (vhost-scoped) | — |
| Dead-letter exchange (DLX) | Dead-letter re-routing | Target queue channel |
Direct Reply-To (amq.rabbitmq.reply-to) | In-connector reply shortcut | Opaque per-channel address |
Vhost / | DefaultVhost segment | amqp.default.{queue} |
Vhost myvhost | Literal segment | amqp.myvhost.{queue} |
A message published over AMQP to queue orders in the default vhost lands in the KubeMQ Queue
channel amqp.default.orders. That channel is interoperable with gRPC/REST queue clients: an
AMQP producer can feed a native KubeMQ consumer on the same channel, and vice-versa. See
Channel mapping for the full grammar.
Canonical Client Example
The examples below use pika 1.x. Key API symbols:
pika.BlockingConnection, pika.URLParameters, pika.ConnectionParameters,
channel.queue_declare, channel.basic_publish, channel.basic_consume,
channel.basic_ack, channel.basic_get.
Work queue (publish + consume)
import pika # pika 1.x
params = pika.URLParameters("amqp://user:YOUR_JWT@kubemq.example.com:5672/")
conn = pika.BlockingConnection(params)
ch = conn.channel()
ch.queue_declare(queue="orders", durable=True)
# Publish
ch.basic_publish(
exchange="",
routing_key="orders",
body=b'{"order_id": "ORD-1"}',
)
# Consume (manual ack)
def on_message(ch, method, props, body):
print("received:", body)
ch.basic_ack(delivery_tag=method.delivery_tag)
ch.basic_qos(prefetch_count=10)
ch.basic_consume(queue="orders", on_message_callback=on_message)
ch.start_consuming()Topic exchange routing
ch.exchange_declare(exchange="logs", exchange_type="topic")
ch.queue_declare(queue="errors", durable=True)
ch.queue_bind(exchange="logs", queue="errors", routing_key="*.error")
ch.basic_publish(exchange="logs", routing_key="app.error", body=b"boom") # routed
ch.basic_publish(exchange="logs", routing_key="app.info", body=b"fyi") # not routedPublisher confirms
ch.confirm_delivery()
ch.basic_publish(
exchange="",
routing_key="orders",
body=b"payload",
mandatory=True,
)
# pika raises UnroutableError on basic.return when mandatory=True and no routeDead-letter exchange
ch.exchange_declare(exchange="dlx", exchange_type="fanout")
ch.queue_declare(queue="dead", durable=True)
ch.queue_bind(exchange="dlx", queue="dead", routing_key="")
ch.queue_declare(
queue="work",
durable=True,
arguments={"x-dead-letter-exchange": "dlx"},
)
# A consumer that basic_nack(..., requeue=False) on "work" dead-letters to "dead"
# with RabbitMQ-exact x-death headers appended.RPC (Direct Reply-To)
import uuid
reply_queue = "amq.rabbitmq.reply-to"
corr_id = str(uuid.uuid4())
# Responder (subscribe to the work queue and reply)
def handle_request(ch, method, props, body):
response = process(body)
ch.basic_publish(
exchange="",
routing_key=props.reply_to,
properties=pika.BasicProperties(correlation_id=props.correlation_id),
body=response,
)
ch.basic_ack(delivery_tag=method.delivery_tag)
# Requester
ch.basic_consume(queue=reply_queue, on_message_callback=on_reply, auto_ack=True)
ch.basic_publish(
exchange="",
routing_key="rpc_queue",
properties=pika.BasicProperties(
reply_to=reply_queue,
correlation_id=corr_id,
),
body=b"hello",
)One-shot pull (basic.get)
method, props, body = ch.basic_get(queue="orders", auto_ack=False)
if method:
print("got:", body)
ch.basic_ack(delivery_tag=method.delivery_tag)
else:
print("queue empty") # note: ~1s latency floor on empty queue (deviation)Security
Authentication — opt-in, default disabled.
Connectors.Amqp.Enable defaults to false. When you enable the connector, the
authentication posture depends on whether Authentication.Enable is set:
- Auth disabled (default): the listener accepts any SASL PLAIN credentials. If KubeMQ is reachable from untrusted networks, either enable authentication or firewall ports 5672/5671.
- Auth enabled: the SASL PLAIN password must be a valid KubeMQ JWT. JWT validation
happens at connect time only — token expiry does not terminate an established connection.
The username is recorded for audit and
user-idchecks.
Authorization. When Casbin is enabled, per-operation checks run against the mapped channel
name (amqp.{vhost}.{queue}). Add policies for the amqp-* client IDs:
# Allow all AMQP clients to publish to the orders queue in the default vhost
allow amqp-.* Write amqp.default.orders
# Allow consumers
allow amqp-.* Read amqp.default.ordersPublish denials silently remove the denied target from the routed set (audited as
amqp.publish.denied). Consume/topology denials return channel.close 403.
TLS. Configure the server's Security block; the AMQPS listener on 5671 then serves the
same certificates as gRPC/REST. Set Connectors.Amqp.Port = 0 to force TLS-only AMQP.
Environment variables:
| Variable | Purpose |
|---|---|
CONNECTORS_AMQP_ENABLE | true to open the listener |
CONNECTORS_AMQP_PORT | Plain TCP listener port (default 5672; 0 disables) |
CONNECTORS_AMQP_TLS_PORT | TLS listener port (default 5671; 0 disables) |
See Authentication & security for JWT issuance and Casbin policy syntax.
What Does NOT Migrate / Deviations
The connector covers the common AMQP 0-9-1 feature set, but a handful of methods are rejected outright, a handful of declare arguments are accepted but never applied, and a handful of behaviors differ intentionally. The three lists below are distinct — the first list errors loudly, the second silently no-ops, and the third changes observable behavior.
Hard rejections (connection.close 540 not-implemented)
These AMQP methods are rejected immediately — your application will receive a protocol-level error and must stop using them:
| Feature | Behavior |
|---|---|
Transactions (tx.select, tx.commit, tx.rollback) | 540 not-implemented — use publisher confirms instead |
Exchange-to-exchange bindings (exchange.bind / exchange.unbind) | 540; capability advertised false |
connection.update-secret | 540 |
basic.recover-async | 540 |
immediate=true on basic.publish | 540 |
Accepted-but-inert arguments (stored, never applied)
The following queue/exchange declare arguments are accepted by the server, stored in topology metadata, surfaced in the dashboard, and logged once per entity — they are silently stored and never alter behavior. If your application relies on these for routing or flow control, that logic must move into the application:
- Alternate exchange (
x-alternate-exchange) - Queue length limits and overflow (
x-max-length,x-max-length-bytes,x-overflow) - Queue expiry (
x-expires) - Queue type (
x-queue-type), lazy mode (x-queue-mode), single-active-consumer (x-single-active-consumer) - Message priority (
x-max-priority)
These arguments do not error — they are accepted and ignored. A queue declared with
x-max-length or x-max-priority behaves like an ordinary queue; the limit or priority is
never enforced. Audit your declares for any reliance on these before you cut over.
Behavioral deviations
These behaviors differ intentionally from RabbitMQ. Review each against your application before migrating.
- Requeue → tail. Requeued messages re-enter at the queue tail. RabbitMQ classic queues preserve near-head position.
- TTL expiry is an eager drop. Expired messages are silently dropped inside the broker —
they are never dead-lettered. The RabbitMQ
expiredDLX trigger does not fire. - TTL clamped. Per-message TTL is clamped to
Queue.MaxExpirationSeconds(default 12h);x-delayis clamped toMaxDelaySeconds. basic.getlatency floor.basic.geton an empty queue has a ~1s latency floor (KubeMQ minimum wait). RabbitMQ returnsget-emptyimmediately. Preferbasic.consume.MaxReceiveCountdrop. Messages redelivered beyond the brokerMaxReceiveCount(default 1024) are dropped or broker-rerouted. RabbitMQ redelivers forever.- DLX trigger — rejected only. DLX fires on the
rejectedtrigger (basic.reject/nackwithrequeue=false) only. Thex-deathheader is synthesized for broker-rerouted poison messages. Theexpiredtrigger does not exist (see deviation 2). - Inert queue arguments. Priority, max-length/overflow,
x-expires, queue-type, and single-active-consumer arguments are inert (see Accepted-but-inert arguments above). - Node-local exclusivity. Exclusive queues and Direct Reply-To are node-local in cluster mode. Exclusive queues from two nodes share the channel name; Direct Reply-To requires the requester and responder on the same node (use load-balancer session affinity).
- Reserved vhost name. The literal vhost name
default(the configuredDefaultVhostvalue) is reserved — reach it via/. Queue and vhost names must not contain; : * >, whitespace, or a trailing.(406/402). - Partial-routing nack. Publisher-confirm
basic.nackon partial routing failure does not roll back queues that already accepted the message. A publisher retry may duplicate messages.
See Capabilities and Error codes for the exhaustive per-method error tables and wire-contract detail.
Verification Smoke Test
Use the work-queue snippet above as a publish-one / consume-one confirmation.
-
Enable the connector. Set
CONNECTORS_AMQP_ENABLE=trueand start (or restart) KubeMQ. Confirm the log linestarted insecure amqp listener(plain TCP) orstarted secure amqp listener(TLS) appears. -
Publish a message.
import pika conn = pika.BlockingConnection(pika.URLParameters("amqp://user:pass@kubemq:5672/")) ch = conn.channel() ch.queue_declare(queue="smoke-test", durable=True) ch.basic_publish(exchange="", routing_key="smoke-test", body=b"hello-kubemq") conn.close() print("published ok") -
Consume the message.
import pika conn = pika.BlockingConnection(pika.URLParameters("amqp://user:pass@kubemq:5672/")) ch = conn.channel() method, props, body = ch.basic_get(queue="smoke-test", auto_ack=True) assert body == b"hello-kubemq", f"unexpected body: {body!r}" conn.close() print("consume ok:", body)
A successful publish followed by a matching consume confirms the connector is reachable and the queue channel is live.
See Also
Migration hub
The ecosystem→connector map, the cross-protocol matrix, and guides for every supported broker.
Getting Started
Connect, declare a queue, and send and receive over AMQP 0-9-1 in a few steps.
Architecture
The everything-is-a-Queue model, virtual exchanges, and cross-protocol interop.
Channel Mapping
The amqp.{vhost}.{queue} grammar, name charset, and the reserved default vhost.
Capabilities
Exactly what is supported, what is inert, and the deviations behind these behaviors.
Configuration reference
The complete Connectors.Amqp.* settings, ports, and TLS options.
Was this page helpful?