Delivery semantics
The previous chapter dropped a word without digging into it: the acknowledgement. Everything turns on its timing. Acknowledge too early and you risk losing a message; acknowledge too late and you risk processing it twice. There is no third door, and this chapter explains why.
In chapter two, we saw what becomes of a message: erased after acknowledgement on the broker, kept and re-readable on the log. We even slipped in a loaded phrase: “acknowledged equals erased”. But we left a gap. Go back to the postal carrier. At what exact instant does he write down “delivered”? The moment he hands you the envelope, or only after you have signed? Between those two instants hides the possibility of an accident, and depending on the choice your message is either lost or received twice. This chapter puts that instant under the microscope and gives you the precise vocabulary to describe what a system really promises when it says “I am delivering this message to you”.
The postal carrier and the instant of the acknowledgement
Go back to the carrier from chapter two. He carries a letter, hands it to you, and at some point writes “delivered” in his notebook, then throws away his copy. That note is the acknowledgement Acknowledgement The signal by which a consumer confirms that a message has been handled, allowing the broker to erase it. Its timing is decisive: acknowledging before processing risks loss (at-most-once), acknowledging after successful processing risks a duplicate on redelivery (at-least-once). As long as no acknowledgement arrives, the broker may redeliver the message. Source: RabbitMQ, Consumer Acknowledgements : the signal that says “this is settled, it can be erased”. Once the copy is gone, no one will ever replay that letter. The acknowledgement is therefore not paperwork: it is the gesture that authorizes forgetting.
The whole question is one word: when? Two instants are possible, and a single accident separates their consequences.
First choice: the carrier writes “delivered” the moment he hands you the envelope, before you have even taken hold of it. If he throws away his copy at that instant and you drop the letter in a puddle right after, it is lost: he keeps no trace, no one will replay it for you. You received the letter at most once, maybe zero times.
Second choice: the carrier waits for your signature before writing “delivered”. But imagine lightning strikes just after you sign, and before he can record your signature in his notebook. From his point of view, the letter is not acknowledged: he will come back tomorrow to deliver the same one. So you will receive it at least once, sometimes twice.
Lose once, or receive twice. This is no minor technicality: it is the heart of delivery semantics, and we are now going to give it its exact names.
The three promises
For a given message, a messaging system always runs three actions: it delivers the message to the consumer, the consumer processes it (this is the real effect: charging a card, reserving stock, sending an email), and someone acknowledges. What changes everything is the order of these actions and what we do on a crash. Three disciplines exist, and only three.
The first puts the acknowledgement before processing. We erase the message as soon as we receive it, then process it. If a crash happens between the acknowledgement and the end of processing, the message is already erased: no one will replay it, the effect will never happen. This is at-most-once delivery At-most-once delivery A guarantee that a message is processed zero or one time, never more. It is obtained by acknowledging the message as soon as it is received, before processing it: a crash between the acknowledgement and the end of processing loses the message, since it has already been erased. It never duplicates, but it can lose. Suited to streams where occasional loss is harmless. Source: Kleppmann, 2017 : the message is processed zero or one time, never more. We never duplicate, but we can lose.
The second puts the acknowledgement after successful processing, and redelivers as long as no acknowledgement has come back. If a crash happens after processing but before the acknowledgement, the system, seeing no acknowledgement, redelivers the message, and the effect happens again. This is at-least-once delivery At-least-once delivery A guarantee that a message is processed one or more times, never zero. It is obtained by acknowledging only after successful processing and redelivering as long as no acknowledgement arrives: a crash after processing but before the acknowledgement triggers a redelivery, hence a duplicate. It never loses, but it can duplicate; this is why the processing must be idempotent. Source: Kleppmann, 2017 : the message is processed one time or more, never zero. We never lose, but we can duplicate.
The third is everyone’s dream: “exactly-once”, neither loss nor duplicate. It is the guarantee we would always like to have. The trouble is that at the delivery level, it is impossible. The next section explains why, and the one after shows how we get close anyway.
Why exactly-once delivery is impossible
Two allied armies camp on two hills, separated by a valley where the enemy stands. To win, they must attack at the same time: if only one charges, it is crushed. The generals can communicate only by sending messengers across the valley, and these messengers may be captured along the way. The first general sends “attack at dawn”. But he will never know whether the messenger got through, so he needs an acknowledgement. The second sends back “agreed, at dawn”. Except this second messenger may be captured too: the second general therefore does not know whether his acknowledgement arrived, and will not dare attack without being sure. We would then need an acknowledgement of the acknowledgement, then an acknowledgement of that one, forever. It can be proved that no protocol with a finite number of messages guarantees that both generals are jointly certain of the same plan.
The link to our messages is direct. The sender can never be absolutely sure that the consumer processed the message exactly once, because the acknowledgement itself, the last messenger, can be lost. Faced with this uncertainty, it has only two possible behaviors: do not retry, and risk loss (at-most-once); or retry, and risk a duplicate (at-least-once). There is no third pure behavior at the delivery level. This is exactly why “exactly-once delivery” is a myth: it is not a limitation of today’s tools, it is a fundamental impossibility.
The way out: at-least-once, plus idempotence
If we cannot guarantee exact delivery, we move the problem. We accept duplicates at delivery (hence at-least-once), but we make them harmless at processing. Processing the same message twice must produce the same result as processing it once. This property is called idempotence Idempotence The property of an operation whose repeated execution produces the same result as a single execution. Applied to messaging, it makes duplicates harmless: an already-processed message is recognized and its effect is not reapplied. It is the mechanism that turns at-least-once delivery into effectively-once delivery. How to concretely build an idempotent consumer (deduplication key, atomicity) is the subject of the next chapter. Source: Hohpe & Woolf, 2003 , and it is what saves the day.
A simple image: pressing the “off” button on an already-off elevator changes nothing; pressing “call floor 3” a second time when it is already requested does not request it twice. The effect is the same whether you press once or ten times. An idempotent processing is the same: the second pass of an already-seen message does not redo the effect, it recognizes it and skips.
The combination “at-least-once plus idempotent processing” has a name: effectively-once delivery Effectively-once delivery The combination of at-least-once delivery and idempotent processing, which makes the observable effect identical to a single processing. Delivery duplicates are not removed but neutralized: an already-processed message is recognized and its effect is not reapplied. It is the realistic approximation of exactly-once, which is impossible at the delivery level because of the two generals problem. Source: Kleppmann, 2017 . We do not remove delivery duplicates, we neutralize them. From the point of view of the observable effect, the payment is charged only once, even if the message arrived twice.
How do we concretely build an idempotent processing, and how do we keep track of already-seen messages without reopening the problem? That is the whole subject of the next chapter: we name it here, we build it there.
Watch the instant of the crash
The component below runs the same message, charging a payment, through all three disciplines at once. You choose the instant of the crash, and you compare.
Start with no crash: all three charge once, all is well. Then place the crash after delivery, before processing: watch at-most-once lose the payment (zero charges), while the other two redeliver and end up at one. Finally place the crash after processing, before acknowledgement, the truly revealing moment: at-least-once charges twice (the duplicate), while effectively-once recognizes the already-seen message and stays at one. That is the whole lesson of the chapter, in one selector.
Choose the instant of the crash, then compare the three disciplines on the same "charge the payment" message. The decisive case is the crash after processing, before acknowledgement.
Instant of the crash
At-most-once
Acknowledges before processing
At-least-once
Acknowledges after processing
Effectively-once
At-least-once, plus idempotent processing
Three questions to ask yourself while playing:
- With the crash “after delivery, before processing”, why is at-most-once the only one to lose the message, even though it has not even charged yet?
- With the crash “after processing, before acknowledgement”, at-least-once and effectively-once both redeliver. What makes one charge twice and the other only once?
- Which is the only column that shows “exactly once” whatever the instant of the crash, and what did it have to add to get there?
In code: Hexeract’s three modes, Wolverine’s safety net
Let us see these disciplines in real code. The surprise is that they are not theories: they are dials you set by hand.
Hexeract, the Rust messaging framework that runs as our common thread, exposes exactly these choices as an acknowledgement mode set on the worker. Its AckMode enum has three variants, and their names speak for themselves.
// At-least-once (the default): we acknowledge AFTER successful processing.
// The broker redelivers as long as no acknowledgement arrives,
// so a duplicate is possible: the handler MUST be idempotent.
let worker = RabbitMqWorkerBuilder::new(connection)
.queue("orders.charge-payment")
.register_handler::<ChargePayment, _>(ChargePaymentHandler)
.ack_mode(AckMode::Manual)
.max_attempts(5) // number of redeliveries before giving up
.build()?;
// At-most-once: we acknowledge ON RECEIPT, before the handler.
// A crash after the acknowledgement and before the handler finishes loses the message.
let logger = RabbitMqWorkerBuilder::new(connection)
.queue("analytics.click")
.register_handler::<ClickEvent, _>(RecordClickHandler)
.ack_mode(AckMode::AckOnReceive)
.build()?;
The comment on the AckMode::Manual variant in Hexeract’s code states verbatim that handlers must be idempotent, because duplicates can occur. That is this chapter proved by code: choosing at-least-once forces you to plan for idempotence on the processing side. A third variant, AckMode::Unacknowledged, pushes the logic to the end: the broker expects no acknowledgement at all, it is all-or-nothing with no safety net, reserved for cases where loss is acceptable.
Wolverine, its .NET counterpart, is instructive because it provides the idempotence safety net out of the box. On a Kafka transport, you ask the consumer to persist each received message before committing its read position, which gives at-least-once delivery; and the same persistence is used to recognize an already-processed identifier and discard it. Effectively-once delivery becomes a single line.
// At-least-once: the message is persisted before committing the offset.
// The same durable inbox deduplicates by message identifier,
// so an already-processed message is discarded: effectively-once.
opts.ListenToKafkaTopic("orders.charge-payment")
.UseDurableInbox();
// You can also retry finely on a transient exception.
chain.OnException<TransientException>()
.RetryWithCooldown(100.Milliseconds(), 250.Milliseconds());
Wolverine’s documentation states it plainly: with the durable inbox, the system automatically discards any message it detects has already been handled, by comparing its identifier to those already seen. Two ecosystems, one same lesson: you explicitly choose your delivery semantic, and effectively-once is never magic, it is at-least-once plus deduplication.
Exercises
Take a sheet of paper and a pencil. The solutions are right below, to look at only after you have tried.
Exercise 1: the right promise for the right message
For our e-commerce order, two very different flows. (a) A stream of click events feeds an analytics dashboard: we receive thousands per minute, and losing a click now and then changes nothing about the trend. (b) Charging the order’s payment: it must be neither forgotten (the customer would not pay) nor done twice (a double charge, a furious customer). For each one, say which delivery semantic you choose and why, in one sentence grounded in the instant of the acknowledgement.
Exercise 2: the accident at the worst moment
A worker receives the message “charge 49 euros”, performs the charge successfully, then its machine shuts down abruptly before it could acknowledge. Describe what happens next (i) if the worker is at-most-once, (ii) if it is at-least-once without idempotence, (iii) if it is at-least-once with idempotent processing. For each case, conclude with the number of times the 49 euros are actually charged.
In one sentence
The instant of the acknowledgement relative to processing decides the guarantee: acknowledging before gives at-most-once (you can lose), acknowledging after gives at-least-once (you can duplicate), exactly-once delivery is impossible (two generals problem), and we imitate it with at-least-once delivery paired with idempotent processing, the effectively-once.
1. What determines the delivery semantic of a message?
2. Why is exactly-once delivery impossible?
3. How do we get an effect applied a single time despite delivery duplicates, in practice?
Towards the next chapter
We now know how to choose our delivery promise, and we have seen that the only safe way to neither lose nor duplicate goes through idempotence. But we invoked it without saying how we build it: how does a worker recognize an “already-processed” message? With what key, stored where, and validated how without reopening the two generals problem inside its own database? Another blind spot: at-least-once delivery redelivers messages, but in what order does it replay them? If “Order delivered” comes back before “Order paid”, idempotence alone is no longer enough. Chapter four, “Ordering and idempotence”, builds the idempotent consumer, its deduplication key, and handles ordering by key and by partition, the piece we carefully set aside in chapter two.
Sources
- Akkoyunlu, E. A., Ekanadham, K. & Huber, R. V. (1975). “Some Constraints and Tradeoffs in the Design of Network Communications.” ACM SIGOPS Operating Systems Review 9(5), 67-74. Original formulation of the two generals problem. DOI 10.1145/1067629.806523
- Kleppmann, M. (2017). Designing Data-Intensive Applications, chapter 9 “Consistency and Consensus” and chapter 11 “Stream Processing” (delivery semantics, idempotence, exactly-once). O’Reilly. Publisher reference
- Apache Kafka. Documentation: Message Delivery Semantics and Exactly-Once Semantics. kafka.apache.org
- RabbitMQ. Consumer Acknowledgements and Publisher Confirms. rabbitmq.com
- Wolverine. Durable Inbox and Idempotent Message Delivery. Official documentation. Durability, Idempotency