KubeMQ
LearnConcepts

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

GuaranteeLoss?Duplicates?CostUse when
at-most-oncePossibleNeverLowest latency, no storageTelemetry, live dashboards, cache invalidation — a missed message is harmless
at-least-onceNeverPossibleStorage + ack round-tripOrders, payments, jobs — losing a message is unacceptable; you can tolerate (or dedup) a repeat
exactly-onceNeverNever (effect)All of the above + dedup/idempotencyFinancial 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 guaranteeKubeMQ patternHow it is provided
at-most-onceEventsFire-and-forget pub/sub — delivered to active subscribers only, no persistence, no retry
at-least-onceEvents StoreEvery message is persisted; durable subscriptions track their position and replay anything missed
exactly-once-processingQueuesExplicit ack/nack settlement + redelivery on nack/timeout + a dead-letter queue; pair with an idempotent handler for effectively-once
request/replyRPCThe 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.

settle.go
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
}
settle.py
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 redelivery
settle.ts
const 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
  }
}
Settle.java
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
    }
}
Settle.cs
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
    }
}
Settle.kt
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
    }
}
settle.cpp
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
    }
}
settle.rs
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
    }
}
settle.rb
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
settle.exs
{: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)
end

The 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 →

Was this page helpful?

On this page