Delivery Guarantees
Understand at-most-once, at-least-once, and exactly-once delivery — plus acknowledgements, redelivery, idempotency, and dead-letter queues.
Imagine mailing a contract. You could drop it in a mailbox and hope it arrives — fast, but if it gets lost you never know. You could send it certified mail, where the courier keeps trying until someone signs for it — nothing is lost, but a frazzled courier might leave two copies. Or you could number every envelope and have the recipient ignore duplicates — slower and more bookkeeping, but each contract lands exactly once.
Those three choices are the three delivery guarantees every messaging system makes you pick between. The guarantee decides what happens when something fails — a subscriber is offline, a network blips, a consumer crashes mid-work. There is no "always perfect" option for free: stronger guarantees cost latency, storage, and complexity. This page explains the three guarantees, the acknowledgement mechanics that make the stronger ones possible, and the safety nets (idempotency and dead-letter queues) that keep them honest.
Delivery guarantees — the idea
A delivery guarantee is a promise about how many times a message is processed by a consumer in the face of failure. There are three:
- at-most-once — deliver the message, never retry. A message is processed zero or one times. Fastest and cheapest; messages can be silently lost.
- at-least-once — keep delivering until the consumer confirms success. A message is processed one or more times. Nothing is lost, but duplicates are possible.
- exactly-once — the message is processed once and only once, even across failures. No loss, no duplicates. The strongest promise and the most expensive to provide.
The pivot between them is the acknowledgement — a small signal the consumer sends back after handling a message. Whether the system waits for an ack, and what it does when an ack never arrives, is what separates the three guarantees.
at-most-once: fire and forget
The broker hands the message to whoever is listening right now and immediately forgets it. There is no ack, no retry, no stored copy. If a subscriber is offline or the message is dropped in transit, it is gone.
at-most-once: the broker delivers to active subscribers only; an offline subscriber misses the message and it is never retried.
at-least-once: acknowledge or redeliver
The broker keeps the message until the consumer acks it. If the consumer fails to ack — it crashes, times out, or sends a nack (negative acknowledgement) — the broker redelivers. Nothing is lost, but a consumer that did the work and then crashed before acking will see the same message again.
at-least-once: with no ack inside the window, the broker redelivers; the message is never lost but may arrive more than once.
exactly-once: acknowledge, then deduplicate
You reach effectively-once by combining at-least-once delivery with deduplication on the consumer side. The consumer records which message IDs it has already handled; a redelivered duplicate is recognized and skipped before it has any effect. The work happens once even though the message may be delivered twice.
exactly-once (effectively-once): at-least-once delivery plus a dedup check on the message ID means the redelivered duplicate has no effect.
Precise definition
A delivery guarantee is the contract a messaging system upholds for how many times a given message is successfully processed when failures occur. It is realized through three mechanics:
- Acknowledgement (ack) — a signal from the consumer that a message was processed successfully and can be discarded by the broker. A negative acknowledgement (nack) signals failure and asks for redelivery.
- Redelivery window — the time the broker waits for an ack before assuming failure and redelivering. (When a consumer holds a message during processing, this is the visibility timeout.)
- Idempotency — a processing operation is idempotent if applying it twice has the same effect as applying it once. Idempotent consumers turn at-least-once delivery into effectively-once results, because duplicates do no extra harm.
A dead letter queue (DLQ) is the final safety net: after a message fails and is redelivered up to a configured maximum number of times, the broker stops retrying and moves it to a separate channel for inspection. This prevents a single "poison" message from being redelivered forever and blocking the queue behind it.
True end-to-end exactly-once across independent systems is impossible in the general case (a consumer cannot atomically both ack the broker and commit a side effect). In practice, "exactly-once" means at-least-once delivery + idempotent processing (or dedup) — often called effectively-once.
Trade-offs
| Guarantee | Loss? | Duplicates? | Cost | Use when |
|---|---|---|---|---|
| at-most-once | Possible | Never | Lowest latency, no storage | Telemetry, live dashboards, cache invalidation — a missed message is harmless |
| at-least-once | Never | Possible | Storage + ack round-trip | Orders, payments, jobs — losing a message is unacceptable; you can tolerate (or dedup) a repeat |
| exactly-once | Never | Never (effect) | All of the above + dedup/idempotency | Financial postings, inventory decrements — a duplicate would corrupt state |
Pitfall: "at-least-once" guarantees delivery, not single processing. If your consumer is not idempotent — for example it blindly increments a balance — a redelivered duplicate will double-charge. Make the handler idempotent (key side effects by message ID, or upsert instead of insert) before relying on at-least-once for anything stateful.
In KubeMQ
In KubeMQ: the guarantee is a property of the pattern you choose, not a per-message flag. Events are fire-and-forget (at-most-once). Events Store persists every message so durable subscribers never lose one (at-least-once on the delivery path). Queues give each consumer an explicit ack / nack decision plus a dead-letter queue after maxReceiveCount retries — the basis for exactly-once-processing when your handler is idempotent. RPC is request/reply: the response is the acknowledgement.
Each KubeMQ pattern occupies a different point on the guarantee spectrum:
| Delivery guarantee | KubeMQ pattern | How it is provided |
|---|---|---|
| at-most-once | Events | Fire-and-forget pub/sub — delivered to active subscribers only, no persistence, no retry |
| at-least-once | Events Store | Every message is persisted; durable subscriptions track their position and replay anything missed |
| exactly-once-processing | Queues | Explicit ack/nack settlement + redelivery on nack/timeout + a dead-letter queue; pair with an idempotent handler for effectively-once |
| request/reply | RPC | The reply confirms processing synchronously; a Command returns an ack, a Query returns data — no separate ack step |
The clearest place to see ack/nack in code is the Queues consumer. After receiving a message, you decide its fate: ack removes it, nack returns it for redelivery. If it is nacked past maxReceiveCount, KubeMQ routes it to the configured dead-letter queue.
resp, err := client.PollQueue(ctx, &kubemq.PollRequest{
Channel: "orders",
MaxItems: 1,
WaitTimeoutSeconds: 5,
})
if err != nil {
log.Fatal(err)
}
for _, m := range resp.Messages {
if err := process(m.Message.Body); err != nil {
m.NAck() // failed — return to queue for redelivery
continue
}
m.Ack() // success — remove from queue
}response = client.receive_queue_messages(
channel="orders",
max_messages=1,
wait_timeout_in_seconds=5,
)
for msg in response.messages:
try:
process(msg.body)
msg.ack() # success — remove from queue
except Exception:
msg.nack() # failed — return to queue for redeliveryconst messages = await client.receiveQueueMessages({
channel: 'orders',
maxMessages: 1,
waitTimeoutSeconds: 5,
});
for (const msg of messages) {
try {
await process(msg.body);
await msg.ack(); // success — remove from queue
} catch {
await msg.nack(); // failed — return to queue for redelivery
}
}ReceiveQueueMessagesResponse response = client.receiveQueueMessages(
ReceiveQueueMessagesRequest.builder()
.channel("orders")
.maxMessages(1)
.waitTimeoutSeconds(5)
.build());
for (QueueMessageReceived msg : response.getMessages()) {
try {
process(msg.getBody());
msg.ack(); // success — remove from queue
} catch (Exception e) {
msg.nack(); // failed — return to queue for redelivery
}
}var response = await client.ReceiveQueueMessagesAsync(new ReceiveQueueMessagesRequest
{
Channel = "orders",
MaxMessages = 1,
WaitTimeoutSeconds = 5,
});
foreach (var msg in response.Messages)
{
try
{
Process(msg.Body);
await msg.AckAsync(); // success — remove from queue
}
catch
{
await msg.NAckAsync(); // failed — return to queue for redelivery
}
}val response = client.receiveQueueMessages(
channel = "orders",
maxMessages = 1,
waitTimeoutSeconds = 5
)
for (msg in response.messages) {
try {
process(msg.body)
msg.ack() // success — remove from queue
} catch (e: Exception) {
msg.nack() // failed — return to queue for redelivery
}
}auto response = client.receiveQueueMessages("orders", 1, 5);
for (const auto& msg : response.messages) {
try {
process(msg.body);
msg.ack(); // success — remove from queue
} catch (const std::exception&) {
msg.nack(); // failed — return to queue for redelivery
}
}let mut receiver = client.new_queue_downstream_receiver().await?;
let response = receiver
.poll(PollRequest {
channel: "orders".to_string(),
max_items: 1,
wait_timeout_seconds: 5,
auto_ack: false,
})
.await?;
for msg in &response.messages {
match process(&msg.message.body) {
Ok(_) => msg.ack().await?, // success — remove from queue
Err(_) => msg.nack().await?, // failed — return to queue for redelivery
}
}receiver = client.create_downstream_receiver
request = KubeMQ::Queues::QueuePollRequest.new(
channel: 'orders', max_items: 1, wait_timeout: 5
)
response = receiver.poll(request)
response.messages.each do |m|
begin
process(m.body)
m.ack # success — remove from queue
rescue StandardError
m.nack # failed — return to queue for redelivery
end
end{:ok, poll} =
KubeMQ.Client.poll_queue(client,
channel: "orders",
max_items: 1,
wait_timeout: 5_000
)
# Settle the whole transaction: ack on success, nack to redeliver
case process(poll.messages) do
:ok -> KubeMQ.PollResponse.ack_all(poll)
_ -> KubeMQ.PollResponse.nack_all(poll)
endThe same receive-then-settle loop in every SDK: ack a processed message, nack a failed one. After maxReceiveCount nacks, KubeMQ moves the message to the dead-letter queue.
How KubeMQ does this →
Events — at-most-once
Fire-and-forget pub/sub: lowest latency, delivered to active subscribers only.
Events Store — at-least-once
Durable pub/sub: every message persisted, durable subscriptions replay anything missed.
Queues — ack / nack / DLQ
Explicit settlement, redelivery, and dead-letter queues for exactly-once-processing.
Ack, Nack & Requeue
The three settlement options in code, with redelivery semantics.
Dead Letter Queue
Route messages to a DLQ after they exceed the max retry count.
RPC — request/reply
The reply is the acknowledgement: Commands return an ack, Queries return data.
Was this page helpful?
Interaction Styles
The three shapes every messaging pattern reduces to — pub/sub fan-out, point-to-point competing consumers, and request/reply round-trips.
Ordering & Replay
How messaging systems preserve order, number every message with a sequence, and let late consumers replay history from any position.