10.1.5 – EOS Anti-Patterns and Debugging
Module: M10 – Advanced Topics & Architecture
Section: 10.1 – Transactions & Exactly-Once Semantics
Goal: Identify common EOS anti-patterns and master debugging transactional issues
Introduction
Implementing exactly-once semantics is complex. This module covers common mistakes, anti-patterns, and debugging techniques to help you avoid and fix transactional issues.
Common Anti-Patterns
Anti-Pattern 1: Non-Unique transactional.id
Problem: Multiple producers using the same transactional.id.
Example:
// ❌ All instances use same transactional.id
Properties props = new Properties();
props.put("transactional.id", "my-producer"); // Same for all instances
// Instance 1 starts
// Instance 2 starts with same ID
// → Instance 1 fenced, fails
Result: Producers constantly fence each other, causing transaction failures.
Solution: Unique transactional.id per logical producer instance.
// ✅ Unique ID per instance
String instanceId = System.getenv("INSTANCE_ID"); // e.g., from Kubernetes pod name
props.put("transactional.id", "my-producer-" + instanceId);
Best Practice:
- Single-threaded app: One
transactional.id - Multi-threaded app: One
transactional.idper thread - Distributed app: One
transactional.idper instance/partition
Anti-Pattern 2: Randomized transactional.id
Problem: transactional.id changes on every restart.
Example:
// ❌ New ID on every restart
props.put("transactional.id", UUID.randomUUID().toString());
Result: Each restart creates a new producer ID, defeating zombie fencing and state recovery.
Solution: Stable transactional.id across restarts.
// ✅ Stable ID (based on consumer partition)
String transactionalId = "pipeline-1-partition-" + partition.partition();
props.put("transactional.id", transactionalId);
Anti-Pattern 3: Transaction Per Message
Problem: Committing a transaction for every single message.
Example:
// ❌ One transaction per message (expensive)
for (Message msg : messages) {
producer.beginTransaction();
producer.send(msg);
producer.commitTransaction(); // 5-10ms overhead per message
}
Result: 10-100x throughput reduction due to transaction overhead.
Solution: Batch messages per transaction.
// ✅ Batch 100-1000 messages per transaction
producer.beginTransaction();
for (Message msg : batch) {
producer.send(msg);
}
producer.commitTransaction(); // Overhead amortized
Rule of Thumb: 100-1000 messages per transaction for optimal throughput.
Anti-Pattern 4: Mixing Transactional and Non-Transactional Writes
Problem: Writing to same topic with both transactional and non-transactional producers.
Example:
Producer A (transactional) → Topic
Producer B (non-transactional) → Same Topic
Result: read_committed consumers see inconsistent data (non-transactional messages visible immediately, transactional messages delayed).
Solution: Use same transaction semantics for all producers writing to a topic.
All producers → transactional (if EOS required)
Or all producers → non-transactional (if EOS not needed)
Anti-Pattern 5: Not Handling ProducerFencedException
Problem: Retrying after being fenced.
Example:
try {
producer.commitTransaction();
} catch (ProducerFencedException e) {
// ❌ Retrying won't help (fenced = terminated)
retry();
}
Result: Producer continues writing with outdated epoch, all writes rejected.
Solution: Close producer immediately on fencing.
catch (ProducerFencedException e) {
log.error("Producer fenced, shutting down", e);
producer.close();
System.exit(1); // Let orchestration restart
}
Anti-Pattern 6: Ignoring Transaction Timeout
Problem: Long-running transactions exceeding transaction.timeout.ms.
Example:
producer.beginTransaction();
// Slow processing (e.g., external API call takes 5 minutes)
for (Message msg : largeNetwork) {
String result = slowApiCall(msg); // 5 min
producer.send(result);
}
producer.commitTransaction(); // ❌ Already timed out
Result: Transaction auto-aborted by coordinator, commit fails.
Solution: Tune transaction.timeout.ms or break into smaller transactions.
// ✅ Option 1: Increase timeout
props.put("transaction.timeout.ms", "600000"); // 10 minutes
// ✅ Option 2: Smaller transactions
for (Batch batch : batches) {
producer.beginTransaction();
processBatch(batch); // < 1 minute
producer.commitTransaction();
}
Anti-Pattern 7: Not Using read_committed with Transactional Producer
Problem: Producer uses transactions, consumer uses read_uncommitted.
Example:
// Producer
props.put("transactional.id", "my-producer");
// Consumer
props.put("isolation.level", "read_uncommitted"); // ❌ Defeats EOS
Result: Consumer sees aborted messages, defeating exactly-once guarantees.
Solution: Match isolation level to producer.
// ✅ Consumer reads committed messages only
props.put("isolation.level", "read_committed");
Anti-Pattern 8: Committing Offsets Outside Transaction
Problem: Committing consumer offsets separately from producing messages.
Example:
producer.beginTransaction();
producer.send(...);
producer.commitTransaction();
consumer.commitSync(); // ❌ Separate commit (not atomic)
Result: Producer commit succeeds but consumer commit fails → messages reprocessed (duplicates).
Solution: Use sendOffsetsToTransaction().
// ✅ Atomic offset commit
producer.beginTransaction();
producer.send(...);
producer.sendOffsetsToTransaction(offsets, consumer.groupMetadata());
producer.commitTransaction(); // Offsets committed atomically
Debugging Transactional Issues
Issue 1: Transactions Constantly Aborting
Symptoms:
- High
txn-abort-totalmetric - Consumer lag increasing
- Logs showing
KafkaExceptionrepeatedly
Possible Causes:
- Network instability (coordinator unreachable)
- Broker failures during transaction
- Application errors (uncaught exceptions)
- Transaction timeout too short
Debugging Steps:
1. Check broker logs:
grep "transaction" /var/log/kafka/server.log
Look for:
TransactionCoordinator: Aborting transactionTransaction timed out
2. Check producer metrics:
// Transaction abort rate
kafka.producer:type=producer-metrics,client-id=*,attribute=txn-abort-total
// Transaction commit time
kafka.producer:type=producer-metrics,client-id=*,attribute=txn-commit-time-avg
Alert If: Abort rate > 5% of total transactions.
3. Increase transaction timeout:
transaction.timeout.ms=300000 # 5 minutes (from 60 seconds)
4. Add detailed logging:
try {
producer.commitTransaction();
log.info("Transaction committed successfully");
} catch (KafkaException e) {
log.error("Transaction failed: {}", e.getMessage(), e);
producer.abortTransaction();
}
Issue 2: Consumer Stuck (Not Making Progress)
Symptoms:
- Consumer lag not decreasing
read_committedconsumer appears frozen- No errors in logs
Possible Causes:
- Stuck transaction (producer didn't commit/abort)
- LSO (Last Stable Offset) not advancing
- Transaction timeout not expired yet
Debugging Steps:
1. Check LSO vs LEO:
# Get log-end-offset (LEO)
kafka-run-class.sh kafka.tools.GetOffsetShell \
--broker-list localhost:9092 \
--topic my-topic \
--time -1 # Latest offset
# Compare with consumer position (LSO)
kafka-consumer-groups.sh --describe \
--group my-group \
--bootstrap-server localhost:9092
If LEO - Consumer Position > 1000, LSO is stuck.
2. Find stuck transaction:
# Check __transaction_state topic
kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic __transaction_state \
--from-beginning \
--formatter "kafka.coordinator.transaction.TransactionLog\$TransactionLogMessageFormatter"
Look for: Transactions in "Ongoing" state for > transaction.timeout.ms.
3. Wait for transaction timeout:
- Transaction coordinator will auto-abort after
transaction.timeout.ms - Default: 60 seconds
- Check broker config:
transaction.max.timeout.ms
4. Restart problematic producer:
- If producer crashed without aborting, restart will trigger abort
Issue 3: Producer Fenced Unexpectedly
Symptoms:
ProducerFencedExceptionin logs- Producer stops writing
- No obvious duplicate instances
Possible Causes:
- Another producer with same
transactional.idstarted - Coordinator failover incremented epoch
- Producer took too long to send
InitProducerIdRequest
Debugging Steps:
1. Check for duplicate instances:
# Search logs for multiple initializations
grep "Invoking InitProducerId" producer.log | wc -l
If > 1, multiple instances are starting with same ID.
2. Check coordinator logs:
grep "Fenced producer" /var/log/kafka/server.log
Look for: Fenced producer pid=12345 with epoch 1 because new producer with epoch 2 started
3. Verify transactional.id uniqueness:
# In Kubernetes
kubectl get pods -o json | jq '.items[].spec.containers[].env[] | select(.name=="TRANSACTIONAL_ID") | .value'
4. Add fencing detection:
catch (ProducerFencedException e) {
log.error("Producer fenced! Instance ID: {}, Transactional ID: {}",
instanceId, transactionalId, e);
// Investigate duplicate instances
producer.close();
System.exit(1);
}
Issue 4: High Latency with read_committed
Symptoms:
- p99 latency increased by 50-100ms
- Consumer processing slow
- No errors
Cause: Waiting for transaction markers (LSO blocking).
Debugging Steps:
1. Measure transaction commit time:
kafka.producer:type=producer-metrics,client-id=*,attribute=txn-commit-time-avg
If > 100ms, transactions are slow.
2. Reduce transaction size:
// Smaller batches = faster commits
int batchSize = 100; // Reduce from 1000
3. Increase consumer timeout:
max.poll.interval.ms=600000 # 10 minutes (tolerate longer waits)
4. Monitor LSO lag:
LEO - LSO = Blocked lag
Alert If: Blocked lag > 1000 messages.
Debugging Tools
1. Transaction CLI Commands
List Active Transactions:
kafka-transactions.sh --bootstrap-server localhost:9092 --list
Output:
transactionalId | state | producerId | producerEpoch | timeout
eos-producer-1 | Ongoing | 1001 | 3 | 60000ms
payment-proc-2 | Complete | 1002 | 1 | 60000ms
Describe Specific Transaction:
kafka-transactions.sh --bootstrap-server localhost:9092 \
--describe --transactional-id eos-producer-1
Output:
TransactionalId: eos-producer-1
ProducerId: 1001
ProducerEpoch: 3
State: Ongoing
TopicPartitions: [orders-0, payments-2, notifications-1]
TransactionStartTime: 2026-02-01T10:15:30Z
TransactionTimeout: 60000ms
Force-Abort a Stuck Transaction:
kafka-transactions.sh --bootstrap-server localhost:9092 \
--abort --transactional-id eos-producer-1
Use Case: Transaction stuck in "Ongoing" state for longer than timeout (zombie producer).
Warning: Only use this if you're certain the producer is dead. Aborting an active producer's transaction will cause it to be fenced.
Check for Zombie Transactions in Logs:
grep "ProducerFencedException" /var/log/kafka/server.log
# Or for transaction timeouts
grep "Transaction timed out" /var/log/kafka/server.log
2. JMX Metrics for EOS Monitoring
Enable JMX on Producer:
export KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote.port=9999 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false"
Critical EOS Metrics Table:
| Metric (MBean) | Purpose | Healthy Value | Alert Threshold |
|---|---|---|---|
kafka.producer:type=producer-metrics,attribute=txn-init-time |
Time to initialize transactions | < 100ms | > 500ms |
kafka.producer:type=producer-metrics,attribute=txn-duration-avg |
Average transaction duration | Scale with batch size | > 10000ms (10s) |
kafka.producer:type=producer-metrics,attribute=txn-commit-time-avg |
Average commit time | < 100ms | > 1000ms |
kafka.producer:type=producer-metrics,attribute=txn-abort-rate |
Aborted transactions per second | 0 (investigate spikes) | > 0.1/sec |
kafka.producer:type=producer-metrics,attribute=txn-timeout-rate |
Transaction timeouts per second | 0 | > 0 |
kafka.consumer:type=consumer-fetch-manager-metrics,attribute=records-lag |
Consumer lag (indicates stalled read_committed consumers) | ~0 (or stable) | Increasing trend |
Access Metrics via JConsole:
jconsole localhost:9999
# Navigate to: MBeans → kafka.producer → producer-metrics
Access Metrics via JMX Exporter (Prometheus):
# Download JMX exporter
wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.17.0/jmx_prometheus_javaagent-0.17.0.jar
# Start producer with JMX exporter
java -javaagent:jmx_prometheus_javaagent-0.17.0.jar=8080:kafka-producer.yml \
-jar my-producer-app.jar
Sample kafka-producer.yml:
rules:
- pattern: kafka.producer<type=producer-metrics, client-id=(.+)><>txn-commit-time-avg
name: kafka_producer_txn_commit_time_avg
type: GAUGE
- pattern: kafka.producer<type=producer-metrics, client-id=(.+)><>txn-abort-rate
name: kafka_producer_txn_abort_rate
type: GAUGE
Grafana Dashboard Query (PromQL):
# Alert on high transaction abort rate
sum(rate(kafka_producer_txn_abort_rate[1m])) by (transactional_id) > 0
# Alert on slow transaction commits
kafka_producer_txn_commit_time_avg > 1000
# Alert on high transaction duration
kafka_producer_txn_duration_avg > 10000
JMX Query via Command Line:
# Using jmxterm
echo "get -b kafka.producer:type=producer-metrics,client-id=my-producer txn-commit-time-avg" | \
java -jar jmxterm.jar -l localhost:9999
3. Transaction State Inspection
View transaction log:
kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic __transaction_state \
--from-beginning \
--formatter "kafka.coordinator.transaction.TransactionLog\$TransactionLogMessageFormatter"
Output:
TransactionalId: my-producer-1
ProducerId: 12345
ProducerEpoch: 1
State: Ongoing
Partitions: [orders-0, payments-2]
Timeout: 60000ms
LastUpdateTimestamp: 1643723400000
4. Consumer Offset Inspection
View committed offsets:
kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic __consumer_offsets \
--formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter"
5. Broker Logs
Enable debug logging:
# log4j.properties
log4j.logger.kafka.coordinator.transaction=DEBUG
log4j.logger.org.apache.kafka.clients.producer=DEBUG
Useful Log Searches:
# Find transaction coordinator activity
grep "TransactionCoordinator" /var/log/kafka/server.log
# Find producer fencing events
grep "Fenced producer" /var/log/kafka/server.log
# Find transaction timeouts
grep "Transaction.*timed out" /var/log/kafka/server.log
6. Monitoring Transaction Health - Complete Checklist
Daily Monitoring:
- Check
txn-abort-rate(should be near 0) - Check
txn-duration-avg(should be < 10s) - Check consumer lag for
read_committedconsumers - Verify no stuck transactions (check
__transaction_state)
Alert Configuration:
# Prometheus AlertManager rules
groups:
- name: kafka_transactions
rules:
- alert: HighTransactionAbortRate
expr: rate(kafka_producer_txn_abort_rate[5m]) > 0.05
for: 5m
annotations:
summary: "Transaction abort rate > 5% for {{ $labels.transactional_id }}"
- alert: SlowTransactionCommits
expr: kafka_producer_txn_commit_time_avg > 5000
for: 5m
annotations:
summary: "Transaction commits taking > 5s for {{ $labels.client_id }}"
- alert: TransactionTimeouts
expr: rate(kafka_producer_txn_timeout_rate[5m]) > 0
for: 1m
annotations:
summary: "Transaction timeouts detected for {{ $labels.transactional_id }}"
Best Practices
1. Monitor Transaction Metrics
// Alert on high abort rate
if (abortRate > 0.05) { // > 5%
alert("High transaction abort rate");
}
// Alert on slow commits
if (commitTimeAvg > 10000) { // > 10 seconds
alert("Slow transaction commits");
}
2. Implement Circuit Breaker
int consecutiveFailures = 0;
int maxFailures = 3;
while (true) {
try {
producer.beginTransaction();
// ... process ...
producer.commitTransaction();
consecutiveFailures = 0; // Reset on success
} catch (KafkaException e) {
consecutiveFailures++;
if (consecutiveFailures >= maxFailures) {
log.error("Circuit breaker tripped after {} failures", maxFailures);
break; // Stop retrying
}
producer.abortTransaction();
}
}
3. Graceful Shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
producer.abortTransaction(); // Abort in-flight transaction
} catch (Exception e) {
log.warn("Error aborting transaction on shutdown", e);
} finally {
producer.close();
consumer.close();
}
}));
Key Takeaways
Unique
transactional.id: Per logical producer instance, stable across restarts.Batch Messages: 100-1000 per transaction to amortize overhead.
Match Isolation Level: Use
read_committedwith transactional producers.Handle Fencing: Close producer immediately on
ProducerFencedException.Transaction Timeout: Tune
transaction.timeout.msfor workload.Atomic Offsets: Use
sendOffsetsToTransaction()for read-process-write.Monitor Metrics: Track abort rate, commit time, and LSO lag.
Debug Tools: Inspect
__transaction_stateand broker logs for issues.
What's Next?
Next Module: 10.1.6 – EOS Performance Tradeoffs
What We Covered:
- ✅ Common EOS anti-patterns
- ✅ Debugging transactional issues
- ✅ Debugging tools and techniques
- ✅ Best practices
What's Coming:
- ❌ Performance impact of transactions
- ❌ Latency vs throughput tradeoffs
- ❌ When to use (and not use) transactions
References:
- Transaction protocol → 10.1.1
- Producer transactions → 10.1.2
- Consumer isolation → 10.1.3
- End-to-end pipelines → 10.1.4