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 mode | Pending tracked | Client action | KubeMQ downstream |
|---|---|---|---|
auto (default) | No | none | reserve → enqueue → immediately ack the delivery; requeue (NAck) if the output buffer is full; never dropped |
client-individual | Yes | ACK/NACK one message | resolve that one delivery → one ack/NAck for its transaction |
client (cumulative) | Yes | ACK/NACK that message and all earlier on the subscription | collect 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 redeliveredclient — 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 drainWhy 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:
| Version | ACK/NACK carries | Resolution |
|---|---|---|
| 1.2 | id:<ack-token> | direct token lookup |
| 1.1 | message-id + subscription | lookup by (message-id, subscription) |
| 1.0 | message-id only | resolves 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:trueReliability 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 closesDISCONNECT 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 closeBroker-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:
| Frame | Broker not ready | Receipt |
|---|---|---|
SEND | ERROR "broker not ready" + close — SENDs do not buffer | no RECEIPT (ERROR path) |
SUBSCRIBE | gated — accepted with an empty subscription id, then activated when the broker becomes ready | RECEIPT 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.
Related
Protocol versions
The per-version MESSAGE field rules and ACK token source — 1.2 id token, 1.1 message-id + subscription, 1.0 oldest pending.
Destination mapping
How /queue/... resolves to the KubeMQ channel that an ACK or NACK acknowledges.
Queues
Competing consumers, redelivery, and the at-least-once guarantee in practice.
Was this page helpful?
Getting Started
Connect a stock STOMP client to KubeMQ and run a publish-and-subscribe round-trip over the Events pattern in minutes — no KubeMQ SDK required.
Authentication
How a STOMP client authenticates to KubeMQ — the CONNECT login/passcode frame, passcode as a KubeMQ JWT, session ids, and per-channel Casbin authorization.