Create your own
Lesson illustration

Delivery Semantics: System-Wide Implications

Hello! Welcome to the final lesson of our module on Distributed Communication Patterns.

Introduction

In our last two lessons, we built a foundation for reliable communication. First, we learned how to make operations idempotent, ensuring that retrying them is safe. Then, we implemented intelligent client-side retries using exponential backoff and jitter, ensuring that our retries are smart and don't overwhelm a struggling system.

These mechanisms—safe retries and smart retries—are not goals in themselves. They are tools we use to achieve specific guarantees about message delivery and processing. Today, we will formalize these guarantees by exploring the three fundamental message delivery semantics in distributed systems. Understanding these is crucial for designing systems that behave predictably, especially when dealing with critical operations like financial transactions.

Learning Outcome:

Analyze the system-wide implications of at-least-once, at-most-once, and exactly-once delivery semantics.

This lesson will connect the patterns we've learned to the high-level guarantees they enable, setting the stage for our hands-on work with message brokers in the next module.


1. Defining the Delivery Semantics

When a producer sends a message to a consumer, especially through an intermediary like a message queue, there are three possible guarantees the system can offer regarding its delivery.

This diagram illustrates the three delivery semantics. 'At-most-once' allows for message loss. 'At-least-once' prevents loss but allows duplicates. 'Exactly-once' is the ideal where each message is processed a single time.

Let's watch a brief video that introduces these concepts in the context of Apache Kafka, a popular distributed streaming platform.

Kafka Delivery Semantics | At-Least-Once, At-Most-Once & Exactly-Once

The video 'Kafka Delivery Semantics' by Code with Irtiza provides a clear, high-level overview of the three delivery modes.

Please watch from 00:33 to 06:08. This covers the definitions and basic mechanisms for at-least-once, at-most-once, and exactly-once delivery. Focus on the core trade-off each semantic represents.

To summarize and formalize what the video explained:

  • At-Most-Once: The producer sends a message and does not wait for confirmation (a "fire and forget" approach). This offers the highest throughput and lowest latency but comes with the risk of message loss if the broker is down or the network fails. The message is delivered 0 or 1 time.

    • Implication: You might lose data.
    • Use Case: Non-critical logging, metrics collection, or broadcasting real-time data where a missed update is quickly superseded by the next one (e.g., a volatile stock price feed).
  • At-Least-Once: The producer sends a message and waits for an acknowledgment from the broker. If an ack isn't received within a timeout, the producer retries. This guarantees the message is never lost but introduces the possibility of duplicates if the original message was delivered but the ack was lost. The message is delivered 1 or more times.

    • Implication: You must handle potential duplicate messages.
    • Use Case: The most common scenario for critical tasks. For example, submitting a trade order or initiating a payment. The system must not lose the request, and downstream services are designed to handle duplicates (e.g., using the idempotency patterns we discussed).
  • Exactly-Once: The system guarantees that each message is delivered and processed precisely one time. This is the most complex and costly guarantee to provide.

    • Implication: Provides the strongest data integrity, simplifying application logic.
    • Use Case: Mission-critical operations where duplicates are unacceptable and difficult to reconcile, such as transferring funds between bank accounts or settling a trade.

2. The Nuance: Delivery vs. Processing

You may have heard that "exactly-once delivery is impossible," often with a reference to the Two Generals' Problem. This is a common point of confusion. The key is to distinguish between message delivery and message processing.

Exactly-once message delivery

The article 'Exactly-once message delivery' by Szymon Pobiega does an excellent job of clarifying this distinction. It argues that focusing on 'processing' makes the problem solvable and more relevant to real-world systems.

Please read the first few sections of the article, from the start down to just before the 'Layers' heading. Focus on the definition of 'message processing' and how it reframes the discussion away from the theoretical impossibility of 'delivery'.

As the article states, a message is processed when all its side-effects are durably persisted. This is what we, as system designers, truly care about. Did the database record get updated? Was the payment completed? Was the outbound notification sent?

With this framing, exactly-once processing becomes an achievable, though challenging, engineering goal. The most common way to achieve it is by combining two elements we've already studied:

At-Least-Once Delivery + Idempotent Consumer = Exactly-Once Processing

The messaging system ensures the message arrives one or more times. The consumer, using a unique identifier from the message (an idempotency key), keeps track of which messages it has already processed. If a duplicate arrives, the consumer acknowledges it without re-processing it, effectively achieving an exactly-once outcome.


3. System-Wide Implications and Trade-offs

Choosing a delivery semantic is a fundamental architectural decision with significant trade-offs across performance, complexity, and cost.

SemanticPerformance (Throughput/Latency)System ComplexityData GuaranteeTypical Use Cases
At-Most-OnceHighest / LowestLowMessages can be lost.Metrics, non-critical notifications, ephemeral state updates.
At-Least-OnceMedium / MediumMedium (Consumer must be idempotent)No message loss, but duplicates can occur.Order processing, task queues, event-driven updates.
Exactly-OnceLowest / HighestHigh (Requires transactional producers/consumers or complex coordination)No message loss, no duplicates.Financial ledgers, payment processing, critical state machines.

As the author of the article "What You Want Is What You Don’t" argues, developers often reach for the strongest guarantees without fully considering the cost. In many cases, designing a robust system with at-least-once delivery and idempotent consumers provides the right balance of reliability and performance. The article puts it well: "Forget about guaranteed delivery and start thinking about idempotence."


4. Deep Dive: Achieving Exactly-Once in Kafka

While the combination of at-least-once delivery and idempotent consumers is a general pattern, some systems like Kafka provide built-in features to facilitate end-to-end exactly-once semantics. This involves more than just the consumer; it requires coordination between the producer, the broker, and the consumer.

This is a complex topic, but given your background, you'll find the mechanics interesting. It involves concepts like idempotent producers, transactional writes, and fencing tokens to handle "zombie" processes.

Kafka Transactions - *Exactly* Once Processing?? | Distributed Systems Deep Dives With Ex-Google SWE

The video 'Kafka Transactions' by Jordan has no life provides a detailed look at the internal mechanisms Kafka uses to enable exactly-once processing. It's a great example of the engineering required to provide such a strong guarantee.

This is a deeper video. Please watch the following segments: The Goal (05:00 - 06:32): Understand the 'input-process-output' cycle and the need for atomic writes. Failure Scenarios & Solutions (07:49 - 11:25): This is the core. Pay close attention to how Kafka uses idempotent producers to prevent duplicate sends and epoch numbers (fencing tokens) to handle zombie consumers. Atomic Writes with Two-Phase Commit (12:49 - 16:19): This explains the transactional mechanism, which uses a coordinator and commit markers to ensure that a series of operations (e.g., writing to an output topic and committing a consumer offset) succeed or fail together.

The key takeaways from this deep dive are:

  • Idempotent Producer: The producer is assigned a unique ID and attaches a sequence number to each batch of messages. The broker tracks the last sequence number it has seen from that producer, discarding any retried batches with the same or an older number.
  • Transactional Guarantees: Kafka allows a consumer to read from an input topic, process the message, and write to an output topic (and commit its read offset) within a single, atomic transaction. This is often called a "read-process-write" cycle.
  • Fencing: To prevent old, disconnected consumers from causing issues (zombies), Kafka's transaction coordinator issues an "epoch" number. The broker will reject any writes from a producer with an outdated epoch, effectively "fencing off" the zombie.

This shows that true exactly-once processing is a sophisticated, system-wide feature that addresses multiple failure modes at every step of the pipeline.


Conclusion

Today we've dissected the three core delivery semantics and analyzed their profound impact on system design. You now understand the trade-offs between them and the mechanisms required to implement them.

Key Takeaways:

  • At-most-once is fast but unreliable, suitable for non-critical data.
  • At-least-once is the reliable default for many systems, preventing data loss but requiring consumers to be idempotent to handle duplicates.
  • Exactly-once processing is the strongest guarantee, eliminating duplicates but at a significant cost to performance and complexity. It is often achieved by combining at-least-once delivery with idempotent consumers.
  • Advanced systems like Kafka offer built-in transactional features to provide end-to-end exactly-once semantics, using mechanisms like idempotent producers, atomic writes, and fencing tokens.

Preview of the Next Lesson:

We have now concluded our module on communication patterns and their associated guarantees. We've covered the theory and the "why." In the next module, "Asynchronous Messaging with Message Brokers," we will move to the "how." We'll start by getting our hands dirty, installing and running a real message broker—RabbitMQ—using Docker. This will be our first step toward building the practical messaging topologies we've been discussing.

Can't find a good explanation? Sign up and we'll make it for you

Sign up