Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 77

Message Delivery Semantics: Trade-offs Explained

Recommended resources

Hello! Welcome back to our series on Event-Driven Architecture with Kafka.

In our last lesson, we tackled the crucial challenge of message ordering. We learned that by using message keys, we can guarantee that related events are processed sequentially by routing them to the same partition. This ensures the sequence of our data is correct.

Now, we shift our focus from order to reliability. What happens if a producer fails to get a confirmation from Kafka? What if a consumer processes a message and then crashes before it can commit the offset? These failure scenarios can lead to either losing data or processing it more than once. This brings us to a fundamental concept in distributed systems and a very common topic in senior-level interviews: message delivery guarantees.

Your learning outcome for this lesson is to explain the trade-offs between different message delivery semantics (at-most-once, at-least-once, and exactly-once). By the end of this hour, you'll be able to confidently discuss which guarantee is appropriate for different use cases and justify the engineering costs associated with each.

1. The Spectrum of Delivery Guarantees

In any distributed messaging system, there are inherent trade-offs between performance and reliability. Kafka exposes these trade-offs through three distinct delivery semantics.

Let's start by getting a clear definition of each.

Exactly-Once Semantics in Kafka

The following article from Conduktor provides a concise and clear explanation of the three delivery guarantees. It lays the foundation for our entire discussion.

Please read the sections 'Understanding Message Delivery Guarantees' and 'What is Exactly-Once Semantics?'. Focus on the core definition of each semantic and the simple failure scenario that leads to it.

Based on your reading, let's break down these concepts in the context of a typical producer-consumer flow.

At-Most-Once: Performance over Reliability

  • Guarantee: Messages might be lost, but they will never be delivered more than once.
  • How it happens:
    • Producer: A producer sends a message but is configured not to retry on failure (e.g., retries=0). If the broker doesn't receive the message due to a transient network issue, the message is lost forever.
    • Consumer: A consumer is configured to commit its offset before it processes the message. It reads a message, commits offset + 1, and then crashes while processing. When it restarts, it will start from the newly committed offset, and the message it was processing is skipped and lost.
  • Trade-off: You get the highest throughput and lowest latency because you're minimizing acknowledgment overhead and retries. The cost is the potential for data loss.
  • Use Cases: This is suitable only for non-critical data where occasional loss is acceptable. For example, collecting trivial metrics (like mouse movements on a webpage) or some types of logging where losing a few entries is not a disaster.

At-Least-Once: Reliability over Duplicates (Kafka's Default)

  • Guarantee: Messages will never be lost, but they might be delivered more than once.
  • How it happens:
    • Producer: A producer sends a message and waits for an acknowledgment (ack). If it doesn't receive one (e.g., due to a network timeout), it assumes failure and retries. The original message might have been successfully written to the broker, so the retry creates a duplicate.
    • Consumer: A consumer reads a message, completes its processing (e.g., writes to a database), and then crashes right before it can commit the offset to Kafka. Upon restart, a new consumer in the group will receive the same message again because the offset was never updated.
  • Trade-off: You get strong reliability against data loss. The cost is that your consuming application must be designed to handle duplicate messages.
  • Use Cases: This is the most common semantic and Kafka's default. It's used in a vast majority of systems where data integrity is important, such as order processing, event sourcing, and critical analytics. The requirement to handle duplicates is pushed to the consumer application.

2. The Gold Standard: Exactly-Once Semantics (EOS)

Exactly-once is the strongest guarantee, promising that the effect of each message processing is observed exactly once. Achieving this in a distributed system is complex and requires coordination across the producer, broker, and consumer.

In Kafka, EOS is not a single setting but a combination of two key features:

  1. Idempotent Producers: Prevents duplicates from producer retries.
  2. Transactions: Allows for atomically writing data to multiple topic partitions and committing consumer offsets as a single unit of work.

This is often discussed in the context of a "read-process-write" application pattern, which is common in stream processing.

Apache Kafka's Exactly-Once Semantics in Spring Cloud ...

The concept of EOS can be subtle. The following blog from the Spring team provides an excellent, nuanced explanation of what 'exactly-once' truly means in a read-process-write cycle.

Read the subsections under 'Exactly-once semantics' and 'Spring Cloud Stream Kafka and Exactly-Once Semantics'. Pay very close attention to the clarification that within an EOS workflow, the 'read' and 'process' steps are actually 'at-least-once'. This is a critical point for a deep understanding.

The "Effectively-Once" Reality

As the article highlights, a key insight for a senior-level discussion is that EOS doesn't mean your processing code literally runs only one time.

  • The guarantee is that the entire read-process-write cycle is atomic. It either succeeds completely, or it's rolled back as if it never happened.
  • If a failure occurs mid-transaction (e.g., during the 'write' phase), the transaction is aborted. The consumer offset is not committed.
  • Upon recovery, the application will re-read and re-process the same message. Therefore, the consume and process steps are themselves at-least-once.

This leads to a crucial conclusion: Even when using Exactly-Once Semantics, your processing logic must be idempotent, especially when interacting with external systems not participating in the Kafka transaction (like a database).

For example, if your 'process' step involves calling an external, non-transactional API, a transactional rollback in Kafka won't undo that API call. Your system must be designed to handle the fact that the API might be called multiple times for the same logical event.


3. Trade-offs and Interview Scenarios

In an interview, you'll be expected to move beyond definitions and justify your choice of delivery semantic based on business requirements and engineering costs.

Exactly-Once Semantics in Kafka

Let's look at the concrete performance costs of choosing exactly-once. This section from the Conduktor article quantifies the trade-offs.

Read the section titled 'Trade-offs and Performance Considerations'. Focus on the impact on latency, throughput, and resource usage.

Here is a summary of the trade-offs, perfect for structuring an interview answer:

Semantic Reliability Performance Complexity Key Requirement on Application
At-Most-Once Low (data loss is possible) Highest (fire-and-forget) Low (simple producer/consumer config) Must tolerate data loss.
At-Least-Once High (no data loss) Medium (acknowledgment and retry overhead) Medium (default config, but requires application logic to be idempotent) Must handle duplicates (idempotency).
Exactly-Once Highest (end-to-end atomicity) Lowest (transactional coordination adds latency) High (requires careful configuration of producer, consumer isolation.level, and transactional logic) Still must handle idempotency for external systems.
Test your understanding!

You are designing a microservices architecture for an e-commerce platform. You need to choose a delivery semantic for the following two event streams. Justify your choice and state the primary obligation it places on your application code.

  1. PaymentProcessed Event Stream: Published by the payment service and consumed by the order service to mark an order as paid.
  2. ProductViewed Event Stream: Published by the front-end and consumed by an analytics service to build a "trending products" dashboard.
Show answer
  1. PaymentProcessed Event Stream:

    • Choice: Exactly-Once Semantics (EOS).
    • Justification: This is a mission-critical financial transaction. Losing a PaymentProcessed event would mean a customer paid but their order is never fulfilled. A duplicate event could potentially lead to duplicate order fulfillment or incorrect accounting. The correctness guarantee of EOS is non-negotiable here, and the performance overhead is an acceptable trade-off for financial integrity.
    • Application Obligation: The order service, when consuming this event, must ensure its database update is part of the same transaction or, if that's not possible, is idempotent. For instance, it should update the order status to 'PAID' only if the current status is 'PENDING_PAYMENT'.
  2. ProductViewed Event Stream:

    • Choice: At-Least-Once Semantics (ALO).
    • Justification: Data loss is undesirable, as it would slightly skew the analytics. However, the cost and complexity of implementing EOS are not justified for this use case. A few duplicate ProductViewed events are acceptable. They might slightly inflate view counts, but this is unlikely to significantly alter the overall "trending" list. ALO provides good reliability without the performance penalty of EOS.
    • Application Obligation: The analytics service must be ableto handle duplicates. For example, if it's counting unique views per user, it could use a data structure like a Set to store (userId, productId) pairs for a given time window, naturally de-duplicating the events.

Conclusion

Understanding message delivery semantics is fundamental to building robust, reliable distributed systems. You've learned not just the definitions, but the "how" and "why" behind them, preparing you for deep technical discussions.

Key Takeaways:

  • At-Most-Once: Fast but lossy. Use only for non-critical data.
  • At-Least-Once: Kafka's default. Reliable against loss but creates duplicates, requiring consumers to be idempotent.
  • Exactly-Once: Provides the strongest guarantee via transactions, ensuring the effect of a message is applied once. It comes at the cost of latency, throughput, and complexity.
  • The most crucial insight is that even with EOS, your application's processing logic often needs to be idempotent to handle transactional rollbacks and interactions with external systems.

Preview of the Next Lesson:
We've repeatedly emphasized the need for idempotent consumers, whether you're using at-least-once or exactly-once semantics. So, how do you actually build one? In our next lesson, we will move from theory to practice and implement an idempotent Kafka consumer to safely process duplicate messages. This is a practical skill that directly addresses the main challenge of at-least-once delivery.