KubeMQ
ConnectorsSTOMPHow-to guides

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.

STOMP has no QoS levels. Delivery reliability is controlled instead by the SUBSCRIBE ack mode, and frame acceptance is confirmed by the optional receipt header. This guide covers both — the three ack modes and which KubeMQ pattern each applies to, how the connector correlates your ACK across STOMP 1.0/1.1/1.2, the ack-timeout requeue, and what a RECEIPT does (and does not) mean.

Ack modes apply to Queues consumption. Events and Events-Store deliver as auto (no client ack) and are at-most-once. For Queues, client-individual is the recommended reliable default — it gives at-least-once with the simplest mental model.

The three ack modes

A SUBSCRIBE carries an ack header; if it is absent the mode defaults to auto. The connector accepts exactly three values — anything else returns ERROR "unknown ack mode" and closes the connection.

ack modePending trackedClient actionKubeMQ downstream
auto (default)Nononereserve → enqueue → immediately ack the delivery; requeue (NAck) if the output buffer is full; never dropped
client-individualYesACK/NACK one messageresolve that one delivery → one ack/NAck for its transaction
client (cumulative)YesACK/NACK that message and all earlier on the subscriptioncollect pending with orderIdx ≤ acked, group by transaction, one ack/NAck per transaction

auto — fire-and-forget

The connector reserves the inflight slot, enqueues the MESSAGE, and immediately acks the delivery downstream. If the per-subscriber output buffer is full it requeues (NAcks) rather than dropping — Queues are at-least-once. No ack token is emitted and no pending state is tracked.

client-individual — per-message ACK

Each MESSAGE must be acknowledged by its own ack token. An ACK resolves exactly that one delivery; a NACK requeues it. This is the recommended reliable default for Queues.

SEND       /queue/jobs/email   (×3)
SUBSCRIBE  /queue/jobs/email   ack:client-individual
  MESSAGE  message-id=m1  ack=tok1  →  ACK id:tok1   (1.2)
  MESSAGE  message-id=m2  ack=tok2  →  ACK id:tok2
  MESSAGE  message-id=m3  ack=tok3  →  ACK id:tok3
queue drains; nothing redelivered

client — cumulative ACK

A cumulative ACK acknowledges the named message and all earlier deliveries on the same subscription. The connector collects every pending entry with orderIdx ≤ the acked delivery, groups them by downstream transaction id, and emits one ack/NAck per transaction.

SEND       /queue/work   (×10)
SUBSCRIBE  /queue/work   ack:client
  ... receive m1..m5 ...
  ACK id:<tok of m5>    → cumulatively acks m1..m5 (grouped by transaction)
  ... receive m6..m10 ...
  ACK id:<tok of m10>   → cumulatively acks m6..m10
queue drains; nothing redelivered after drain

Why one range per transaction matters. A single subscription's pending set routinely spans multiple downstream transactions. The connector emits one ack request per transaction id so that no foreign-transaction sequence is silently dropped — this is correctness, not an optimization. It is transparent to your client code.

Per-version ACK/NACK correlation

How the connector matches your ACK or NACK frame to a tracked delivery depends on the negotiated version:

VersionACK/NACK carriesResolution
1.2id:<ack-token>direct token lookup
1.1message-id + subscriptionlookup by (message-id, subscription)
1.0message-id onlyresolves to the oldest pending delivery on the connection

The ack token is a 1.2-only opaque UUID, emitted on the MESSAGE frame and distinct from message-id. On 1.1 you ACK by message-id + subscription; on 1.0 by message-id alone, which the connector maps to the oldest pending delivery.

ACK resolution is connection-scoped — a token that belongs to another connection (even a same-login one) is silently ignored, as is a late ACK after the ack-timeout expired, and an ACK on an events subscription (accepted as a no-op; the RECEIPT, if requested, still fires).

Lenient 1.0. The connector accepts client-individual and NACK on 1.0 too (ActiveMQ-style leniency), even though those are not in the 1.0 spec. Don't rely on this for portability — prefer accept-version:1.2. See Protocol versions.

Ack timeout, requeue, and no client-side DLQ

If a client-individual or client delivery is not ACKed within the ack-timeout (default 30 s), a 1-second sweeper requeues (NAcks) the delivery — it does not disconnect the client. A late ACK after expiry is silently ignored. The same requeue happens when the client disconnects mid-stream with un-ACKed deliveries.

Ack-timeout = 30 s → requeue (not disconnect); there is no client-side DLQ. The connector surfaces redelivery only via the redelivered:true MESSAGE header. There is no STOMP-level dead-letter queue or redelivery-limit knob — a maxReceiveCount / DLQ is broker queue-channel configuration, not a STOMP feature. A second consumer that receives a redelivered message sees redelivered:true; it never arrives on a client-side DLQ.

SUBSCRIBE  /queue/rq   ack:client-individual
  MESSAGE  job-1   (NOT ACKed)
  ... 30s elapse → sweeper requeues ...
# a second consumer:
SUBSCRIBE  /queue/rq   ack:client-individual
  MESSAGE  job-1   redelivered:true

Reliability semantics

Queues are at-least-once; Events and Events-Store are at-most-once. Never exactly-once.

  • Queues = at-least-once. Duplicates are tolerated, never lost: requeue on a full output buffer, sweeper requeue on ack-timeout, and a disconnect NAcks pending deliveries. Design Queue consumers to be idempotent.
  • Events / Events-Store = at-most-once. A full per-subscriber output buffer drops that one delivery for that one subscriber — the connection stays alive and no requeue happens (events have no ack channel).

Receipts

The receipt header is honored on every processed client frame (SEND, SUBSCRIBE, UNSUBSCRIBE, ACK, NACK, DISCONNECT). The connector enqueues a RECEIPT receipt-id=<id> frame after the frame is processed.

A RECEIPT means "KubeMQ accepted the frame", not "a consumer received the message". For a SEND, the RECEIPT fires after the publish returns successfully — it confirms the message was accepted by KubeMQ, not that any subscriber has consumed it.

SEND  /topic/demo  receipt:r-1
  RECEIPT  receipt-id=r-1     # KubeMQ accepted the SEND (NOT consumer delivery)

ERROR-path frames never send a RECEIPT

A handler that closes the connection on an ERROR path never sends a RECEIPT — the STOMP spec permits an ERROR in lieu of a RECEIPT. So a SEND to a bad destination carrying a receipt: header returns an ERROR frame and no RECEIPT. Client code that waits for a RECEIPT must also handle the ERROR-then-close case.

SEND  /badprefix//  receipt:r-2
  ERROR  message:invalid destination     # no RECEIPT; connection closes

DISCONNECT receipt — the clean-shutdown confirmation

A DISCONNECT carrying receipt:<id> flushes its RECEIPT to the wire before the socket closes, deterministically. This is the clean way to confirm a graceful shutdown — the examples in this connector use it.

DISCONNECT  receipt:bye
  RECEIPT  receipt-id=bye     # flushed before close

Broker-not-ready: SEND closes, SUBSCRIBE gates

When the message broker is not ready, the connector treats SEND and SUBSCRIBE asymmetrically — worth knowing because it surprises STOMP migrants who expect buffering:

FrameBroker not readyReceipt
SENDERROR "broker not ready" + close — SENDs do not bufferno RECEIPT (ERROR path)
SUBSCRIBEgated — accepted with an empty subscription id, then activated when the broker becomes readyRECEIPT still sent on acceptance

So a publisher hitting a not-ready broker is disconnected (retry the connection), while a subscriber's SUBSCRIBE is held and activated transparently on recovery.

Was this page helpful?

On this page