9.1 Exactly-Once Processing in Kafka Streams
Module Overview
Goal: Master exactly-once semantics (EOS) in Kafka Streams.
What You'll Learn:
- What exactly-once means
- How Kafka Streams achieves EOS
- EOS v1 vs EOS v2 (ALOS vs EOS)
- Transactional processing
- Producer transactions and consumer offsets
- Performance implications
- Configuration and best practices
What's NOT Covered Here:
- General delivery semantics (at-most-once, at-least-once) — conceptually covered, but deep producer/consumer specifics are in their respective modules
- State stores (covered in Module 6.1)
- Changelog topics (covered in Module 6.2)
Key Insight: EOS is the authoritative source — all concepts related to transactions, commits, idempotency, and exactly-once guarantees live here.
1. Understanding Delivery Semantics
The Three Guarantees
AT-MOST-ONCE:
├─ Messages delivered 0 or 1 times
├─ May lose messages
└─ No duplicates
AT-LEAST-ONCE:
├─ Messages delivered 1 or more times
├─ No data loss
└─ May have duplicates
EXACTLY-ONCE:
├─ Messages delivered exactly 1 time
├─ No data loss
└─ No duplicates
Why Exactly-Once is Hard
Distributed system challenges:
1. Network failures
├─ Producer sends message
├─ Broker receives it
└─ Ack lost → producer retries → duplicate
2. Multiple operations
├─ Read from Kafka
├─ Process data
├─ Write to Kafka
├─ Commit offset
└─ Any failure → inconsistency
3. State updates
├─ Update state store
├─ Write to changelog
└─ Failure → state mismatch
Exactly-once requires atomicity across ALL these
2. Kafka Streams Exactly-Once Approach
The Solution: Transactions
Kafka Streams uses transactions to make operations atomic:
1. Read-process-write as atomic unit
├─ Read records
├─ Process (update state, write output)
├─ Commit offsets
└─ ALL or NOTHING
2. Idempotent producer
└─ Prevents duplicates from retries
3. Transactional reads
└─ Consumers only see committed data
Together: Exactly-once semantics
High-Level Flow
Transaction boundary (commit.interval.ms):
BEGIN TRANSACTION
├─ Read records from input topic
├─ Process records
├─ Update state store
├─ Write to changelog topic
├─ Write to output topic
├─ Commit consumer offsets
COMMIT TRANSACTION
All succeed → visible to downstream
Any failure → rollback → retry
3. Enabling Exactly-Once Semantics
Configuration
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "my-app");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// Enable exactly-once semantics (v2, recommended)
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
// Alternatives:
// StreamsConfig.AT_LEAST_ONCE (default, no EOS)
// StreamsConfig.EXACTLY_ONCE (v1, older, slower)
EOS v1 vs EOS v2
EOS V1 (Kafka < 2.5):
├─ Uses transactional producer per task
├─ Zombie fencing via transaction.id
├─ Slower (more transactions)
└─ Higher overhead
EOS V2 (Kafka 2.5+):
├─ Uses single transactional producer per thread
├─ Zombie fencing via group.instance.id
├─ Faster (fewer transactions)
├─ Lower overhead
└─ Preferred for production
Use EXACTLY_ONCE_V2 unless on older Kafka
4. How Exactly-Once Works
Components
1. IDEMPOTENT PRODUCER:
├─ Enabled automatically with EOS
├─ Producer ID + sequence number
└─ Broker deduplicates retries
2. TRANSACTIONS:
├─ Atomic writes across partitions
├─ Transaction coordinator tracks state
└─ All-or-nothing commit
3. TRANSACTIONAL READS:
├─ Consumer reads only committed data
├─ isolation.level=read_committed
└─ Uncommitted data invisible
4. OFFSET COMMIT IN TRANSACTION:
├─ Offsets committed as part of transaction
├─ Read and write atomic
└─ No offset/output mismatch
Visual Flow
┌────────────────────────────────────────────────┐
│ KAFKA STREAMS INSTANCE │
│ │
│ BEGIN TRANSACTION │
│ ├─ Read: offset 100-105 (input-topic) │
│ ├─ Process: aggregate, transform │
│ ├─ Write: state store (local) │
│ ├─ Write: changelog topic (transactional) │
│ ├─ Write: output topic (transactional) │
│ └─ Commit: offset 106 (transactional) │
│ COMMIT TRANSACTION │
│ │
│ Result: All visible atomically or none │
└────────────────────────────────────────────────┘
5. Idempotent Producer
What is Idempotence?
Idempotence = same operation multiple times → same result
Producer idempotence:
├─ Producer ID (PID) assigned by broker
├─ Sequence number per partition
└─ Broker tracks (PID, sequence) → deduplicates
Example:
├─ Producer sends record (PID=5, seq=10)
├─ Broker receives, ack lost
├─ Producer retries (PID=5, seq=10)
└─ Broker: "Already received seq=10, ignore" → no duplicate
Configuration
// Idempotent producer enabled automatically with EOS
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
// Internally sets:
// enable.idempotence=true
// acks=all
// max.in.flight.requests.per.connection=5 (default, can be higher)
// retries=Integer.MAX_VALUE
Limitations
Idempotence guarantees:
├─ Within a single producer session
├─ No duplicates from retries
└─ Order preserved per partition
Does NOT guarantee:
├─ Across producer restarts (PID changes)
├─ Multiple producers
└─ Exactly-once by itself (needs transactions)
Use transactions for true exactly-once
6. Transactions
What is a Transaction?
Transaction = atomic unit of work across multiple partitions
Properties (ACID):
├─ Atomicity: All writes succeed or none
├─ Consistency: Valid state transitions
├─ Isolation: Uncommitted data invisible
└─ Durability: Committed data persists
Kafka Streams transaction includes:
├─ Output topic writes
├─ Changelog topic writes
├─ Consumer offset commits
Transaction Lifecycle
1. BEGIN:
└─ Producer sends BeginTransaction
2. WRITE:
├─ Write to output topics (uncommitted)
├─ Write to changelog topics (uncommitted)
└─ All writes marked with transaction ID
3. COMMIT:
├─ Producer sends CommitTransaction
├─ Transaction coordinator marks committed
└─ Data visible to consumers
4. ABORT (on failure):
├─ Producer sends AbortTransaction
├─ Transaction coordinator marks aborted
└─ Data never visible to consumers
Transaction Coordinator
Transaction Coordinator (broker-side):
├─ Tracks transaction state
├─ Manages transaction log (__transaction_state)
├─ Coordinates commits/aborts
└─ Ensures atomicity
Transaction flow:
Producer → Transaction Coordinator → Brokers
7. Transactional Reads
Read Committed Isolation
// Automatically set with EOS
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
// Internally sets consumer config:
// isolation.level=read_committed
// Consumer only reads committed data
// Uncommitted data buffered until commit/abort
How It Works
Timeline:
1. Producer begins transaction
2. Writes records to topic (uncommitted)
3. Consumer polls
└─ Records buffered (not returned)
4. Producer commits transaction
5. Consumer polls
└─ Records now visible and returned
Aborted transaction:
└─ Records discarded, never visible
Performance Impact
WITH read_committed:
├─ Consumer waits for transaction commit
├─ Higher latency (buffering)
└─ Guaranteed no uncommitted data
WITHOUT read_committed (read_uncommitted):
├─ Consumer reads immediately
├─ Lower latency
└─ May read data from aborted transactions
Trade-off: Correctness vs latency
8. Commit Interval
What is Commit Interval?
commit.interval.ms = how often to commit transactions
Default: 30 seconds (30000 ms)
Behavior:
├─ Process records for commit.interval.ms
├─ Commit transaction (output + offsets + changelog)
└─ Repeat
Smaller interval:
├─ More frequent commits
├─ Lower latency
├─ Higher overhead
Larger interval:
├─ Fewer commits
├─ Higher latency (data visible less often)
├─ Lower overhead
Configuration
// Commit every 10 seconds
props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 10000);
// Commit every minute (low overhead)
props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 60000);
// Commit every 5 seconds (low latency)
props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 5000);
Trade-offs
SHORT COMMIT INTERVAL (e.g., 5 seconds):
✅ Lower end-to-end latency
✅ Smaller transactions
✅ Faster recovery (less replay)
❌ Higher overhead (more commits)
❌ Lower throughput
LONG COMMIT INTERVAL (e.g., 60 seconds):
✅ Higher throughput
✅ Lower overhead
❌ Higher latency (data visible less often)
❌ Slower recovery (more replay)
9. Exactly-Once Across Application Boundaries
Internal vs External
WITHIN KAFKA STREAMS (INTERNAL):
✅ Exactly-once guaranteed
├─ Kafka → Kafka Streams → Kafka
└─ All transactional
EXTERNAL SYSTEMS:
❌ Exactly-once NOT guaranteed automatically
├─ Kafka → Kafka Streams → Database/API
└─ Requires additional work
Achieving External Exactly-Once
Two approaches:
1. IDEMPOTENT WRITES:
├─ External system deduplicates
├─ Example: Upsert to database (same key overwrites)
└─ Application-level idempotence
2. TRANSACTIONAL OUTBOX PATTERN:
├─ Write to Kafka + DB in single transaction
├─ Outbox table in DB
├─ Separate process reads outbox → external system
└─ Complex but reliable
Simplest: Design external writes to be idempotent
10. Zombie Fencing
The Zombie Problem
Scenario:
1. Instance A processes task 0
2. Network partition (A appears dead)
3. Rebalance: Instance B assigned task 0
4. Network recovered: Instance A alive (zombie!)
5. Both A and B process task 0 → duplicates
Solution: Zombie fencing
How Zombie Fencing Works
EOS v1 (transaction.id)
1. Each task gets unique transaction.id
└─ Format: {application.id}-{task.id}
2. Broker tracks transaction.id → producer epoch
└─ Latest epoch wins
3. Zombie writes rejected
├─ Old epoch → ProducerFencedException
└─ Zombie shuts down
Downside: One producer per task (high overhead)
EOS v2 (group.instance.id)
1. Static membership (group.instance.id)
└─ Instance has fixed ID
2. Single producer per thread (not per task)
└─ Lower overhead
3. Zombie fencing via consumer group protocol
├─ New instance joins → fences old
└─ Zombie's writes rejected
Advantage: Fewer producers, better performance
Configuration (EOS v2)
// Static membership for zombie fencing
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
// Optionally set static group instance ID
// (Usually auto-assigned)
props.put(ConsumerConfig.GROUP_INSTANCE_ID_CONFIG, "instance-1");
// Higher session timeout for stability
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 45000);
11. Performance Implications
Overhead of EOS
Compared to at-least-once:
LATENCY:
├─ +10-30% higher
└─ Transaction commit overhead
THROUGHPUT:
├─ -10-20% lower
└─ Transaction coordination overhead
CPU:
├─ Slightly higher
└─ Transaction management
Memory:
├─ Similar
└─ No significant difference
Network:
├─ Slightly higher
└─ Transaction metadata
When to Use EOS
USE EXACTLY-ONCE WHEN:
✅ Duplicates unacceptable (financial, billing)
✅ Correctness critical
✅ Can tolerate latency overhead
✅ Kafka 2.5+ (EOS v2)
USE AT-LEAST-ONCE WHEN:
✅ Duplicates acceptable
✅ Idempotent downstream processing
✅ Need maximum throughput
✅ Latency sensitive
Optimizing EOS Performance
1. Use EOS v2 (not v1)
└─ 3-5× faster than v1
2. Tune commit interval
└─ Balance latency vs throughput
3. Increase batch size
└─ props.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, ...)
4. Use compression
└─ Reduce network overhead
5. Ensure broker capacity
└─ Transaction coordination needs resources
12. Failure Scenarios
Scenario 1: Producer Failure Mid-Transaction
Timeline:
1. BEGIN transaction
2. Write records (uncommitted)
3. Producer crashes (no commit)
4. Transaction timeout (transaction.timeout.ms)
5. Broker aborts transaction
6. New producer starts
7. Resumes from last committed offset
8. Reprocesses records (exactly-once maintained)
Result: No data loss, no duplicates
Scenario 2: Broker Failure
Timeline:
1. Producer writes to broker (uncommitted)
2. Broker crashes
3. Producer retries (idempotent)
4. Writes to new leader
5. Commits transaction
6. Data visible
Result: Exactly-once maintained
Scenario 3: Consumer Rebalance
Timeline:
1. Instance A processing task 0
2. Instance B joins → rebalance
3. Task 0 reassigned to B
4. A's in-flight transaction aborted
5. B starts from last committed offset
6. Reprocesses uncommitted records
Result: Exactly-once maintained
13. Monitoring EOS
Important Metrics
# Transaction state
kafka.streams:type=stream-thread-metrics,
thread-id=*,
name=commit-latency-avg
# Failed transactions
kafka.streams:type=stream-thread-metrics,
thread-id=*,
name=failed-stream-threads
# Producer metrics (transaction)
kafka.producer:type=producer-metrics,
client-id=*,
name=txn-commit-time-avg
# Consumer lag (should be low)
kafka.consumer:type=consumer-fetch-manager-metrics,
client-id=*,
name=records-lag-max
Checking Transaction State
# List transaction state topics
kafka-topics.sh --bootstrap-server localhost:9092 --list | grep transaction
# __transaction_state topic (internal)
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
--topic __transaction_state \
--from-beginning \
--formatter kafka.coordinator.transaction.TransactionLog\$TransactionLogMessageFormatter
14. Common EOS Issues
Issue 1: ProducerFencedException
Error:
org.apache.kafka.common.errors.ProducerFencedException
Cause:
├─ Zombie instance writing with old epoch
└─ Transaction ID fenced by newer producer
Solution:
├─ Instance should shut down (automatic)
└─ Ensure static membership configured correctly
Issue 2: TransactionCoordinatorFencedException
Error:
org.apache.kafka.common.errors.TransactionCoordinatorFencedException
Cause:
├─ Transaction coordinator changed
└─ Broker rebalanced
Solution:
├─ Retry (automatic)
└─ Ensure broker stability
Issue 3: High Commit Latency
Symptom: Slow transaction commits
Causes:
├─ High broker load
├─ Large transactions
├─ Network latency
└─ Many partitions
Solutions:
1. Scale brokers
2. Reduce commit interval (smaller transactions)
3. Optimize network
4. Reduce partition count (if excessive)
15. Best Practices
1. Use EOS v2
// Always use v2 (unless Kafka < 2.5)
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
2. Tune Commit Interval
// Balance latency vs throughput
// Start with 30s, adjust based on requirements
props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 30000);
// Low latency use case
props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 5000);
// High throughput use case
props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 60000);
3. Monitor Transaction Health
Track:
├─ Commit latency
├─ Failed transactions
├─ ProducerFencedExceptions
└─ Consumer lag
Alert on anomalies
4. Design Idempotent External Writes
// If writing to external system
// Ensure writes are idempotent
// BAD: Increment counter
// db.execute("UPDATE counters SET count = count + 1 WHERE id = ?");
// GOOD: Set exact value
// db.execute("UPDATE counters SET count = ? WHERE id = ?", newCount, id);
// Or use upserts, deduplication keys, etc.
5. Test Failure Scenarios
Test cases:
├─ Kill instances mid-processing
├─ Rebalance during transaction
├─ Broker failures
└─ Network partitions
Verify no duplicates, no data loss
16. Mental Model Summary
Exactly-Once in Kafka Streams
┌───────────────────────────────────────────────────┐
│ EXACTLY-ONCE SEMANTICS (EOS) │
│ │
│ Components: │
│ ├─ Idempotent producer (no duplicates) │
│ ├─ Transactions (atomic writes) │
│ ├─ Transactional reads (read_committed) │
│ └─ Offset commits in transaction │
│ │
│ Result: │
│ └─ Read → Process → Write → Commit (atomic) │
│ │
│ Failure: │
│ └─ Abort → Retry → Exactly-once maintained │
└───────────────────────────────────────────────────┘
EOS v1 vs EOS v2
EOS V1:
├─ One producer per task
├─ transaction.id fencing
├─ High overhead
└─ Legacy (Kafka < 2.5)
EOS V2:
├─ One producer per thread
├─ group.instance.id fencing
├─ Lower overhead
└─ Recommended (Kafka 2.5+)
Transaction Flow
BEGIN TRANSACTION
↓
READ (input topics)
↓
PROCESS (transform, aggregate)
↓
WRITE (state store + changelog + output)
↓
COMMIT (offsets + transaction)
↓
VISIBLE (downstream consumers)
17. Questions You Should Be Able to Answer
After this module, you should be able to answer:
✅ What is exactly-once semantics?
✅ How does Kafka Streams achieve exactly-once?
✅ What's the difference between EOS v1 and EOS v2?
✅ What is an idempotent producer?
✅ What are transactions in Kafka?
✅ How does zombie fencing work?
✅ What is isolation.level=read_committed?
✅ What's the performance impact of EOS?
✅ When should I use exactly-once vs at-least-once?
18. What's Next?
Now that you understand exactly-once semantics, the next modules will cover:
- Module 10.1: Error handling — handling failures with EOS
- Module 11.1: Interactive queries — querying state with EOS
- Module 12.1: Monitoring — tracking EOS metrics
Summary
You learned:
- ✅ Exactly-once = no data loss + no duplicates
- ✅ Achieved via idempotent producer + transactions + transactional reads
- ✅ EOS v2 preferred (faster, lower overhead, Kafka 2.5+)
- ✅ Transactions make read-process-write-commit atomic
- ✅ Zombie fencing prevents duplicate processing from old instances
- ✅ commit.interval.ms controls transaction frequency (latency vs throughput)
- ✅ Overhead: 10-30% latency, 10-20% throughput reduction
- ✅ Use EOS for critical applications (finance, billing, compliance)
- ✅ External exactly-once requires idempotent writes or outbox pattern
Next up: Module 10.1 — Error Handling & Failure Scenarios — Learn how to handle errors gracefully in Kafka Streams applications.