{"post":{"slug":"exactly-once-delivery-2026","title":"Exactly-Once Delivery: The Guarantee You Cannot Have","excerpt":"Message systems advertise exactly-once semantics. What is actually achievable is at-least-once delivery plus idempotent processing, which produces the same outcome by a different route.","category":"code","date":"January 7, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">A</span> sender delivers a message and waits for acknowledgement. The acknowledgement is lost in transit. The sender does not know whether the message arrived, so it either resends — risking a duplicate — or does not — risking loss.\n\nThere is no third option. Any protocol has this problem at its last step, and adding more acknowledgements just moves it. Exactly-once *delivery* is not achievable across an unreliable channel, and no amount of engineering makes it so.\n\n## What changed in 2026\n\n- **The framing shifted to exactly-once effects.** Vendor messaging moved toward describing outcomes rather than delivery, which is more honest.\n- **Idempotent consumers became the standard pattern.** Building for duplicates rather than trying to prevent them became the default assumption.\n- **Broker-level deduplication improved within boundaries.** Systems offering it within their own transactional scope got better, with the boundary limitation better understood.\n- **Retention windows got documented.** The finite nature of deduplication state became a design consideration rather than a surprise.\n\n## Delivery versus effects\n\nThe useful distinction.\n\n**Exactly-once delivery** means the message is transmitted and received precisely once. Impossible, for the reason above.\n\n**Exactly-once effects** means the *result* of processing occurs once, regardless of how many times the message arrives. Entirely achievable, and it is what you actually wanted.\n\n| Guarantee | Achievable | How |\n| --- | --- | --- |\n| At-most-once | Yes | Do not retry; accept loss |\n| At-least-once | Yes | Retry until acknowledged |\n| Exactly-once delivery | No | — |\n| Exactly-once effects | Yes | At-least-once plus idempotency |\n\nSo the design is: deliver at least once, and make processing idempotent so duplicates are harmless. The messaging layer guarantees arrival; the consumer guarantees the effect happens once.\n\n## The deduplication key\n\nIdempotent processing needs a way to recognise a message it has already handled, and where that key comes from matters.\n\n**The producer must supply it.** A key generated by the receiver on arrival is different for each delivery of the same message, which makes it useless for deduplication.\n\n**It must be stable across retries.** If the producer generates a new identifier when retrying, the retry looks like a new message.\n\n**It should be meaningful where possible.** An order identifier, a transaction reference, or a natural business key is more useful than a random value, because it deduplicates across producers and retries alike.\n\nRecording processed keys and skipping anything already seen turns duplicate delivery into a no-op — the same mechanism as [idempotency explained](/blog/idempotency-explained-2026).\n\n## The retention problem\n\nDeduplication state cannot grow forever. You must eventually forget processed identifiers, and the moment you forget one, a very delayed duplicate would be processed again.\n\nSo deduplication is bounded by a retention window. Choose one longer than any plausible delivery delay — including the broker's maximum retry period and any dead-letter reprocessing — and accept that a duplicate arriving after it is theoretically possible.\n\nFor most systems a window of days is comfortably sufficient. Where it is not, the alternative is making the operation naturally idempotent rather than relying on deduplication state: an upsert keyed on a business identifier, or a state transition checking current state, needs no memory of past messages at all.\n\nThat is the stronger design where it applies — see [upsert patterns](/blog/upsert-patterns-explained-2026).\n\n## Common mistakes\n\n- **Trusting an exactly-once claim across a system boundary.** Broker guarantees stop at the broker.\n- **Receiver-generated deduplication keys.** Cannot recognise duplicates.\n- **Unstable producer keys.** Retries look like new messages.\n- **Unbounded deduplication state.** Grows forever.\n- **Ignoring ordering.** Duplicates and reordering are separate problems.\n- **Idempotency checks not atomic with the effect.** A crash between check and effect reintroduces the duplicate.\n- **At-most-once by accident.** Acknowledging before processing loses messages on failure.\n\n## FAQ\n\n**What about brokers advertising exactly-once?**\nThey typically provide it within their own transactional scope — read, process, and write back inside one transaction the broker controls. That is genuine and it stops at the boundary. A side effect in an external system is outside the guarantee.\n\n**Should I acknowledge before or after processing?**\nAfter, for at-least-once. Acknowledging first gives at-most-once, where a crash after acknowledgement loses the message. At-least-once with idempotency is nearly always the right choice.\n\n**How do I handle ordering?**\nSeparately — deduplication does not address it. Partition by a key so related messages are ordered, and include a version or timestamp so a late-arriving older message can be recognised and ignored.\n\n**What about messages that fail repeatedly?**\nA dead-letter queue after a retry limit, so a poison message does not block the stream. Reprocessing from there must respect the same deduplication window.\n\n## Where to go next\n\nFor the idempotency mechanism, read [idempotency explained](/blog/idempotency-explained-2026) and [upsert patterns](/blog/upsert-patterns-explained-2026). For the webhook case of the same problem, [webhook signing](/blog/webhook-signing-explained-2026).","hue":312,"metaTitle":"Exactly-Once Delivery: The Guarantee You Cannot Have","metaDescription":"Why exactly-once message delivery is impossible, how at-least-once plus idempotency achieves the same outcome, and how to choose a deduplication key.","keywords":["exactly once delivery","at least once messaging","idempotent consumer","message deduplication","two generals problem","distributed messaging","queue semantics","distributed systems 2026"],"tldr":["**Exactly-once delivery is impossible across an unreliable network** — the acknowledgement can always be lost.","**At-least-once plus idempotent processing gives you the outcome you wanted** — effects happen once even if messages arrive twice.","**The deduplication key must come from the producer** — a receiver-generated key cannot recognise a duplicate.","**Deduplication state needs a retention window** — you cannot remember every message forever.","**Skip:** relying on a broker's exactly-once mode across a system boundary it does not control."],"readTime":"4 min read","wordCount":799},"related":[{"slug":"message-queues-explained-2026","title":"Message queues explained in 2026: when, why, and which one","excerpt":"Message queues decouple producers from consumers and absorb traffic spikes, but picking the wrong broker costs months. Here is the 2026 decision guide.","category":"code","date":"March 2, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":308},{"slug":"webhook-retry-and-idempotency-design-2026","title":"Webhook Retry and Idempotency Design in 2026","excerpt":"Webhooks are delivered at-least-once, which means every consumer must handle retries and duplicates safely. This guide covers event ID dedup, fast acknowledgment, and idempotent handler design for 2026.","category":"code","date":"August 10, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":304},{"slug":"what-is-a-message-broker-2026","title":"What Is a Message Broker in 2026: A Practical Guide","excerpt":"Message brokers decouple services so they do not have to talk to each other directly. Here is how they work and how to choose between the major options.","category":"code","date":"July 7, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":320}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"disaggregated-serving-2026","title":"Disaggregated Serving: Separating Prefill From Decode","excerpt":"Processing a prompt and generating tokens have opposite hardware profiles. Running them on the same machine means one of them is always using the wrong resource.","category":"ai","date":"January 7, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":219},"older":{"slug":"gin-indexes-explained-2026","title":"GIN Indexes: Indexing Values Inside a Column","excerpt":"A B-tree indexes a column's value. A GIN index indexes the things inside it — array elements, document keys, words in a text field — which is a different problem.","category":"code","date":"January 7, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":316}}