Home DevOps Kafka CommitFailedException — Stop Rebalance Loops
Advanced 5 min · September 23, 2026

Kafka CommitFailedException — Stop Rebalance Loops

CommitFailedException means a rebalance revoked your partitions mid-poll.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 21 min
  • Basic Kafka consumer concepts
  • Familiarity with consumer groups
  • Access to consumer logs and metrics
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • CommitFailedException means the group rebalanced and revoked your partitions before you committed — your offsets now belong to someone else
  • The top cause is slow record processing exceeding max.poll.interval.ms (default 5 minutes), making the consumer look dead
  • Faster polling (max.poll.records), async processing, and static membership with cooperative rebalancing stop the spiral
  • Never ignore it and continue — the new owner reprocesses your records, so design every handler idempotent
✦ Definition~90s read
What is Kafka CommitFailedException Fix?

CommitFailedException (Kafka's CommitFailedException, surfaced in librdkafka-based clients as commit failures with _INVALID_ARG or rebalance callbacks) means your consumer called commitSync (or the auto-commit fired) for partitions the group coordinator already revoked in a rebalance. The mechanism is group membership: consumers heartbeat on a background thread (session.timeout.ms, default 10s in modern clients) and must call poll frequently enough that processing plus polling fits inside max.poll.interval.ms (default 5 minutes).

Imagine movers splitting warehouse rooms and checking in regularly.

Exceed the interval and the coordinator assumes the consumer died, triggers a rebalance, reassigns its partitions to survivors, and rejects the late commit — because accepting it would corrupt the new owner's offset tracking.

Two distinct death paths converge here. Slow processing: each poll returns up to max.poll.records (default 500), and if handling them takes longer than max.poll.interval.ms, the consumer misses its liveness deadline while doing legitimate work. Missed heartbeats: GC pauses, blocked event loops, or network stalls stop the heartbeat thread past session.timeout.ms, and the coordinator evicts the member even though processing was fine.

Both end identically — revoked partitions, failed commit, replayed records.

What this error is NOT: it's not a broker outage (brokers healthy; the group simply rebalanced), not a serialization failure (those throw before any commit attempt), and not fixed by committing more aggressively (committing revoked partitions always fails). It specifically testifies that ownership moved before the commit — so the investigation targets processing time per poll, heartbeat health, and rebalance configuration, never broker hardware or message format.

Plain-English First

Imagine movers splitting warehouse rooms and checking in regularly. One mover hits a packed room and goes quiet too long. The foreman assumes they quit and reassigns their rooms. When the slow mover finally reports done, the foreman says 'not your rooms anymore' — that's CommitFailedException. Fix it by clearing rooms faster, checking in more often, warning you'll be slow (a bigger max.poll.interval.ms), or issuing permanent name tags so absences don't trigger reassignment.

Your Kafka consumer logs CommitFailedException, then reprocesses 40,000 records it already handled — sending duplicate emails, double-counting metrics, and triggering fraud alerts on transactions that settled an hour ago. The rebalance that caused it took 90 seconds, reassigned all 12 partitions, and repeated 6 times before anyone noticed the loop.

This failure loves slow handlers: a consumer that calls a 2-second fraud API per record, processes 500 records per poll, and exceeds the 5-minute max.poll.interval.ms without calling poll again. The broker declares it dead, rebalances, and the revived consumer's commit lands on partitions it no longer owns. Each rebalance makes the backlog bigger, processing slower, and the next rebalance sooner — a death spiral.

The trap is catching the exception and continuing as if the commit succeeded. Offsets never committed, the new owner replays from the last commit, and your 'handled' records process twice. Combined with auto-commit enabled, teams get the worst of both worlds: commits firing mid-rebalance and duplicates everywhere.

By the end of this article you'll read rebalance logs like a timeline, size poll intervals from measured processing time, configure static membership and cooperative rebalancing, and build idempotent handlers plus transactional produces that make rebalances boring instead of catastrophic.

Why the Coordinator Revokes First and Asks Later

The group coordinator can't distinguish a dead consumer from a slow one — both stop calling poll. Its only evidence is silence past max.poll.interval.ms, and its only safe action is revocation: reassign the silent member's partitions to survivors so the group keeps making progress. Your consumer's late commit is then correctly rejected, because the new owner has already started fetching from the last committed offset and accepting yours would fork the offset history.

This design makes rebalances load-bearing for correctness. Offsets are per-partition progress markers shared by whoever owns the partition; two owners with divergent commit sequences would corrupt consumption guarantees for the whole group. The coordinator prefers duplicate processing (new owner replays from last commit) over lost processing (accepting a stale commit that skips records) — which is why every rebalance replays uncommitted work by construction, and why idempotent handlers are mandatory rather than nice-to-have.

Read rebalance logs as ownership transfers with timestamps: Revoked partitions [...] marks what you lost, Assigned partitions [...] marks the new map, and the gap between JoinGroup and SyncGroup is the stop-the-world pause (90s in this article's incident under eager rebalancing). Frequent revoke-assign cycles with no membership changes scream slow processing; cycles aligned with deploys scream missing static membership.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Read the rebalance story from consumer logs (ownership timeline)
grep -E 'Revoked|Assigned|JoinGroup|SyncGroup|CommitFailed|Heartbeat failed' /var/log/app/consumer.log | tail -30
# Pattern A (slow processing): Revoked WITHOUT any deploy/restart nearby,
#   poll-start..poll-end gaps >> max.poll.interval.ms
# Pattern B (deploy storm): Revoked exactly at rollout timestamps, all members

# Measure the actual offense: time one poll batch end-to-end
# Add temporary markers (or use existing poll metrics):
# logger.info('poll-start', {count: records.count})
# ... process ...
# logger.info('poll-end', {elapsed_ms})
awk '/poll-start/{s=$1} /poll-end/{e=$1; print "batch:", e-s}' /var/log/app/consumer.log | tail -5
# Verdict: batch time must fit in max.poll.interval.ms with 2x+ margin

# Live lag view: which partitions stall while rebalances churn?
kafka-consumer-groups --bootstrap-server $BROKERS --describe --group orders
⚠ Never Swallow CommitFailedException and Continue
A failed commit means another consumer owns those partitions now. Continuing to process 'your' records duplicates everything the new owner already replays. Treat the exception as lost ownership — stop, re-poll, and let the rebalance protocol reassign cleanly.
📊 Production Insight
Alert on rebalance frequency per group (more than 2 per 10 minutes is abnormal for stable membership). Rebalance storms are the earliest signal — they precede the duplicate side effects by tens of minutes.
🎯 Key Takeaway
Revocation protects offset correctness; replays are by design. Read revoke/assign timestamps to separate slow-processing from deploy-driven rebalances.

Size the Poll Loop: Records, Latency, and the 5-Minute Wall

The governing inequality is brutally simple: max.poll.records times worst-case seconds-per-record must fit inside max.poll.interval.ms with margin. Default 500 records at a 2-second downstream call needs 1,000 seconds against a 300-second wall — mathematically guaranteed rebalancing, no mystery, no broker fault. Every CommitFailedException of the slow-processing class is this arithmetic failing, and the fix is changing one of the three numbers: fewer records, faster records, or a longer wall.

Prefer fewer records first. Cutting max.poll.records to 50 turns the incident's 1,000s batch into 100s — inside budget with 3x margin — at the cost of more frequent polls (cheap) instead of rebalances (catastrophic). Speed up records second: timeouts on downstream calls, caching repeat verdicts, batching external calls. Lengthen the wall last: raising max.poll.interval.ms to 10 minutes accommodates genuinely slow work but delays failure detection for truly dead consumers — a real availability tradeoff, not a free knob.

Validate with production-like dependency latency, never mocks. The 50ms staging mock hid a 40x processing difference that only production traffic revealed. Load-test consumers with dependency latency injected at p99-plus, measure batch times at the 99th percentile (not the mean), and demand 2x margin under the interval before shipping.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# The governing arithmetic — run it with MEASURED numbers, not hopes
# batch_seconds = max.poll.records * p99_seconds_per_record  (must be < interval/2)
# Incident: 500 * 2.0s = 1000s vs 300s wall -> guaranteed rebalance
# Fixed:     50 * 0.8s = 40s   vs 300s wall -> 7x margin

# Consumer config that respects the arithmetic (properties style)
# max.poll.records=50
# max.poll.interval.ms=300000
# session.timeout.ms=10000
# max.poll.interval must exceed batch p99 with 2x+ margin

# Load-test with production-like dependency latency (NOT mocks)
# tc qdisc add dev eth0 root netem delay 2000ms  # staging consumer host
# Run consumer, measure batch p99 from poll-start/poll-end markers:
awk '/poll-start/{s=$NF} /poll-end/{print $NF-s}' /var/log/app/consumer.log | sort -n | awk '{a[NR]=$1} END{print "p99 batch:", a[int(NR*0.99)], "s"}'
# Ship only if p99 batch < max.poll.interval.ms / 2
📊 Production Insight
Downstream timeouts are poll-loop arithmetic too: an 800ms fraud-call cap turns unbounded 2s+ records into bounded 0.8s ones, shrinking batch p99 more than any record-count cut alone.
🎯 Key Takeaway
records times per-record latency must fit in the poll interval with 2x margin. Cut records first, speed records second, lengthen the wall last.

Heartbeats vs Processing: Two Liveness Signals, Two Failures

Modern Kafka clients separate liveness into two channels: a background heartbeat thread governed by session.timeout.ms (dead in ~10s of silence) and the poll-call cadence governed by max.poll.interval.ms (dead in ~5 minutes without poll). Slow processing kills via the interval; GC pauses, event-loop blocks, and network stalls kill via heartbeats — even when processing itself is fast. Treating both as 'consumer slow' misdiagnoses half of all rebalance incidents.

Heartbeat deaths leave distinctive evidence: 'Heartbeat failed' and group-rebalancing warnings with healthy poll-batch timings, correlated with GC pause spikes, full event-loop lag metrics, or coordinator-connection resets. The consumer was doing fine work and got evicted for going quiet on the wrong channel. Fixes target the stall (GC tuning, unblocking the loop, connection resilience) plus modest session.timeout.ms headroom — not smaller poll batches, which change nothing for a heartbeat problem.

Monitor both channels independently. Track poll-batch p99 against the interval (processing health) and heartbeat success rate plus GC pause p99 (heartbeat health) on the same dashboard. When rebalances strike, the channel that degraded first names the cause — and the fix list for each channel shares almost nothing, which is why the distinction matters more than any single tuning value.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Separate heartbeat death from processing death (same incident, different fixes)
# 1. Heartbeat evidence: failures with HEALTHY batch times?
grep -c 'Heartbeat failed' /var/log/app/consumer.log
awk '/poll-start/{s=$NF} /poll-end/{print $NF-s}' /var/log/app/consumer.log | sort -n | tail -3
# Heartbeat failures + small batches -> stall (GC/loop/network), NOT slow work

# 2. Correlate with GC pauses (JVM consumers)
grep 'Pause Young\|Pause Full' /var/log/app/gc.log | awk '{print $NF}' | sort -nr | head -5
# Pauses near session.timeout.ms (10s) evict members mid-processing

# 3. Heartbeat-side config (only after ruling out slow processing)
# session.timeout.ms=15000        # modest headroom, not 60s (delays real failure detection)
# heartbeat.interval.ms=3000     # ~1/3 of session timeout, the standard ratio
# max.poll.interval.ms=300000    # leave alone — this knob is for processing, not heartbeats
📊 Production Insight
A single 12-second GC pause evicts a member with default 10s session timeout — then the 'recovered' consumer rejoins and triggers a second rebalance. Two rebalances from one pause is the signature of heartbeat-class failure.
🎯 Key Takeaway
Poll interval polices processing speed; session timeout polices heartbeat continuity. Diagnose the failing channel first — their fixes don't overlap.

Static Membership and Cooperative Rebalancing for Calm Deploys

Every rolling deploy without static membership is a voluntary rebalance storm: each restarting consumer rejoins as a stranger, the coordinator revokes and reassigns, and with eager rebalancing the entire group pauses while every member reclaims partitions. Four consumers restarting sequentially means four stop-the-world pauses — during which lag grows, batches swell, and the slow-processing trap arms itself for the aftermath.

Static membership (group.instance.id set to a stable, unique-per-instance value) lets a restart rejoin as the same member and keep its partitions — no revocation, no reassignment, no storm. Kubernetes StatefulSets (stable pod names) or persisted instance IDs provide the stability; bare Deployments with random pod names need the ID plumbed from stable storage or the downward API. Pair it with the cooperativeSticky assignor so the rebalances that do occur migrate only the delta partitions while the rest keep consuming — turning 90s full pauses into sub-second partial ones.

Session timeout interacts here too: with static membership, a truly dead instance's partitions wait for session.timeout.ms before reassignment (availability cost), so keep it tight (10-30s) rather than inflating it as a rebalance band-aid. The combination — stable IDs, cooperative assignment, tight sessions — makes deploys invisible and reserves rebalances for genuine failures.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Calm-deploy consumer config (properties style)
# group.instance.id=orders-consumer-${HOSTNAME}  # STABLE + UNIQUE per instance
# partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
# session.timeout.ms=10000
# heartbeat.interval.ms=3000
# max.poll.interval.ms=300000

# Kubernetes: stable identity via StatefulSet pod name (deployments get random names)
# env:
# - name: HOSTNAME
#   valueFrom: { fieldRef: { fieldPath: metadata.name } }
# group.instance.id = f"orders-{HOSTNAME}"  # same pod name -> same member -> no revoke

# Verify: rolling restart should show (almost) zero revocations
kubectl rollout restart statefulset orders-consumer
grep -cE 'Revoked|CommitFailed' /var/log/app/consumer.log  # expect ~0 during rollout
kafka-consumer-groups --bootstrap-server $BROKERS --describe --group orders  # lag flat
🔥group.instance.id Must Be Stable AND Unique
Two instances sharing one ID fence each other out of the group (duplicate member); one instance changing ID every restart rejoins as a stranger (rebalance storm). Stable per-instance identity — StatefulSet pod names, not random Deployment hashes — is the whole game.
📊 Production Insight
Fenced-instance errors ('member with same instance id') after scaling events mean ID reuse across live instances. Key the ID to something truly instance-unique (pod UID persisted to a volume) rather than a role name or replica index alone.
🎯 Key Takeaway
Static membership preserves partitions across restarts; cooperativeSticky migrates only deltas. Together they delete deploy-driven rebalance storms.

Idempotent Handlers: Replays Are Guaranteed, Duplicates Are Optional

Kafka's delivery contract under rebalancing is at-least-once: the new owner replays from the last committed offset, so every uncommitted record processes twice. This isn't a bug to eliminate — it's the protocol's chosen tradeoff (duplicates over data loss). Your handler design decides whether replays are harmless no-ops or $18,400 double-charge incidents. Idempotency is therefore not an optimization; it's the correctness layer the transport deliberately leaves to you.

Build guards keyed on the record's natural idempotency key (order ID, event ID): conditional writes (DynamoDB attribute_not_exists, SQL INSERT ... ON CONFLICT DO NOTHING), processed-set lookups with TTLs covering the maximum replay window, or state-machine transitions that refuse to re-apply terminal states. The guard and the mutation must be atomic — check-then-act across two calls races when two owners briefly overlap during the handoff.

Commit strategy complements but never replaces guards. commitSync after each batch gives clean replay boundaries (replay from batch start) at throughput cost; async commits need rebalance-listener hooks committing on revoke to shrink the replay window. Auto-commit on a timer fires mid-batch and widens replay unpredictably — disable it for any handler with side effects. But remember the hierarchy: guards make duplicates safe, commits merely make replays smaller.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Idempotency guard shape (DynamoDB conditional write — atomic check+act)
# key = record.order_id; the write itself refuses replays:
# table.put_item(
#   Item={'order_id': oid, 'status': 'CHARGED', 'ts': now},
#   ConditionExpression='attribute_not_exists(order_id)')
# except ConditionalCheckFailedException:
#   metrics.incr('duplicate_suppressed'); return  # replay absorbed, zero side effects

# Commit hygiene for side-effect handlers (disable timer auto-commit)
# enable.auto.commit=false
# commitSync() after each successfully guarded batch
# rebalance listener: on_partitions_revoked -> commitSync() (shrink replay window)

# Verify replays are absorbed, not just committed (downstream metrics, not lag)
# duplicates_suppressed rate should RISE during rebalances while charges stay flat
kafka-consumer-groups --bootstrap-server $BROKERS --describe --group orders | awk '{print $1, $5, $6}'
# Lag draining + flat charge counts + rising suppressed = healthy replay handling
📊 Production Insight
Size processed-set TTLs to cover the worst replay window (largest batch plus longest rebalance), not the average. A TTL shorter than a 90s-rebalance replay lets the same record double-process after its guard expires.
🎯 Key Takeaway
At-least-once means replays are certain; atomic idempotency guards make them harmless. Disable timer auto-commit for side-effect handlers.

Exactly-Once With Idempotent Producers and Transactions

For consume-process-produce pipelines (read order, write receipt to another topic), idempotent handlers alone leave a gap: the consume offset and the produced record commit separately, so a crash between them duplicates the produce or loses the offset. Kafka transactions close the gap by atomically committing consumed offsets and produced records together — the downstream sees the receipt exactly when the offset advances, and aborted transactions are invisible to read_committed consumers.

The setup has four mandatory pieces: enable.idempotence=true on the producer (sequence-checked, duplicate-free writes within a session), a unique transactional.id per producer instance (stable across restarts for fencing zombies), sendOffsetsToTransaction inside the consume loop coupling offsets to output, and isolation.level=read_committed on every downstream reader (otherwise uncommitted transactional records leak through). Miss any piece and you have complexity without the guarantee — the worst outcome.

Adopt transactions where duplicates cost money (payments, inventory, billing events) and accept their price: added end-to-end latency, transaction-coordinator load, and operational surface (stuck transactions need abort monitoring, zombie fencing needs stable IDs). For everything else — notifications, metrics, search indexing — idempotent handlers plus read-committed consumers deliver the practical safety at a fraction of the cost. Match the guarantee to the damage.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Transactional consume-process-produce checklist (all four or nothing)
# Producer: enable.idempotence=true
#           transactional.id=orders-tx-${STABLE_INSTANCE_ID}  # unique + stable (fences zombies)
# Consumer: isolation.level=read_committed  (on EVERY downstream reader)
# Loop: producer.beginTransaction()
#         -> produce receipt record
#         -> producer.sendOffsetsToTransaction(offsets, group_id)
#       producer.commitTransaction()   # offsets + output commit atomically
#       # abortTransaction() on ANY exception — never leave transactions dangling

# Ops: watch for stuck/old transactions (abort + alert, they block read_committed readers)
kafka-transactions --bootstrap-server $BROKERS --describe --all 2>/dev/null | head -20
# Downstream lag on read_committed readers that never drains often means
# an open transaction pinning the log-start offset — find and abort it.
📊 Production Insight
Zombie fencing via stable transactional.id is the feature that justifies transactions for money paths: a resurrected old instance can't commit over its replacement. Unstable (random-per-boot) IDs silently disable this protection.
🎯 Key Takeaway
Transactions atomically commit offsets with output for money paths; they demand all four pieces plus abort monitoring. Idempotent handlers cover the rest.
● Production incidentPOST-MORTEMseverity: high

The Fraud Check That Rebalanced 12 Partitions 6 Times

Symptom
At 10:15 AM, order-processing lag spiked from 2s to 47 minutes while consumer logs filled with CommitFailedException and 'Heartbeat failed: group is rebalancing' warnings. Each rebalance reassigned all 12 partitions across 4 consumers, took ~90s, and replayed every uncommitted record — 40,000 duplicate order attempts in one hour. Downstream, 312 customers were double-charged before the payments team froze captures at 11:20 AM, with $18,400 in refunds issued by end of day.
Assumption
The team assumed broker instability because rebalances 'mean Kafka problems' in their mental model, and spent 40 minutes checking broker GC, disk, and network — all healthy. They then blamed a new consumer deploy from 9:50 AM and rolled it back twice; the rollback changed nothing because the deploy only added logging. Nobody timed the poll loop for an hour because 'the fraud API was fast in staging' — where it answered in 50ms against a mock.
Root cause
Each poll fetched 500 records (default max.poll.records) and the handler called the production fraud API synchronously at ~2s per record under load — 1,000s of processing per poll against a 300s max.poll.interval.ms. The coordinator declared the consumer dead 5 minutes into every poll, rebalanced its partitions to a sibling (which then faced the same 500-record wall), and rejected the late commit. Six consecutive rebalances in the hour replayed the backlog repeatedly, and the non-idempotent charge path billed every replay.
Fix
Four changes shipped in sequence. First, max.poll.records dropped to 50 and the fraud call got an 800ms timeout with a fallback verdict, cutting worst-case poll processing to ~60s inside the 300s budget. Second, handlers were made idempotent with an order-ID-keyed processed-set (DynamoDB conditional write), so replays absorb harmlessly. Third, static membership (group.instance.id) plus cooperativeSticky assignment stopped whole-group stop-the-world rebalances during deploys. Fourth, a poll-interval-ratio alert pages when processing time exceeds 50% of max.poll.interval.ms — the 17-minute polls would have paged on the first occurrence.
Key lesson
  • Size max.poll.records from measured per-record time, not defaults. Five hundred records times real-world latency must fit inside max.poll.interval.ms with margin — the arithmetic takes two minutes and prevents the entire spiral.
  • Staging mocks hide production latency. A 50ms mock versus a 2s production dependency is a 40x processing difference — load-test consumers against production-like dependency latency, not mocks.
  • Rebalances replay uncommitted work by design, so non-idempotent handlers convert every rebalance into duplicate side effects. Idempotency isn't optional for consumers — it's the cost of admission.
Production debug guideFive checks — timing, heartbeats, membership, assignment, safety — that break the rebalance loop.5 entries
Symptom · 01
CommitFailedException with no obvious cause and rebalances in the logs
Fix
Prove the poll loop overruns the interval with measured numbers: grep -E 'CommitFailed|rebalance|Revoked|Heartbeat failed' /var/log/app/consumer.log | tail -20; then time one poll batch (log poll-start/poll-end timestamps) and compute records_per_poll seconds_per_record vs max.poll.interval.ms. If 500 records 2s = 1000s against a 300s limit, the diagnosis is complete — cut max.poll.records to fit (e.g. 50 records * 2s = 100s) before touching anything else.
Symptom · 02
Processing looks fast but heartbeats still fail and members get evicted
Fix
Separate heartbeat health from processing speed: check consumer lag per partition (kafka-consumer-groups --bootstrap-server $BROKERS --describe --group orders | awk '$5 > 10000') to confirm stall, then look for heartbeat-thread starvation — long GC pauses (grep 'GC pause' app gc logs), blocked event loops, or network blips to the coordinator. If heartbeats die while processing is quick, raise session.timeout.ms modestly, fix the GC/loop stall, and verify with a steady heartbeat log before resizing polls.
Symptom · 03
Every deploy or rolling restart triggers a full rebalance storm
Fix
Check for missing static membership and eager assignment: grep -E 'group.instance.id|partition.assignment.strategy' /etc/app/consumer.properties (or your client config). Empty group.instance.id means every restart rejoins as a stranger and revokes everything. Set group.instance.id to a stable pod-unique value (e.g. ${HOSTNAME}), session.timeout.ms to ~10-30s, and partition.assignment.strategy to cooperativeSticky so only migrating partitions pause instead of the whole group stopping for 90s.
Symptom · 04
Rebalances replay records and cause duplicates or double charges
Fix
Stop the damage path first, then harden: check whether handlers are idempotent (grep for dedupe keys, conditional writes, or processed-sets — absence means every replay duplicates). Immediately narrow the blast radius by cutting max.poll.records so replays shrink, then add an idempotency guard (order-ID conditional write) and verify with kafka-consumer-groups --describe showing lag draining without duplicate side-effect counts rising in your downstream metrics.
Symptom · 05
You need exactly-once guarantees across consume-process-produce flows
Fix
Verify idempotent producer plus transactions are actually enabled end-to-end: grep -E 'enable.idempotence|transactional.id|isolation.level' consumer/producer configs — need enable.idempotence=true, a unique transactional.id per instance, and read_committed on downstream readers. Test with kill -9 mid-batch in staging and confirm zero duplicates downstream. Transactions add latency and ops surface, so adopt them only where duplicates cost money; idempotent handlers cover everything else.
CommitFailedException Causes — How to Confirm and Fix Each
Root CauseHow to ConfirmFixPrevention
Slow processing exceeding max.poll.interval.msBatch time (records x latency) exceeds the interval; revokes without deploysCut max.poll.records, cap downstream calls, then extend interval if neededShip only with 2x margin proven under production-like latency
Heartbeat starvation (GC, blocked loop, network)Heartbeat failures with healthy batch times; GC/loop metrics correlateFix the stall; modest session.timeout.ms headroom with 1:3 heartbeat ratioDashboard poll health and heartbeat health as separate signals
Deploy-driven storms without static membershipRevokes align with rollout timestamps across all membersStable group.instance.id plus cooperativeSticky assignmentVerify near-zero revocations on every rolling restart
Eager rebalancing pausing the whole group90s full pauses on every membership change; lag spikes group-wideSwitch to cooperativeSticky so only delta partitions migrateChaos-test member bounces in staging and measure pause duration
Non-idempotent handlers duplicating on replayDuplicates correlate with rebalances; no dedupe guard in handler codeAtomic idempotency guards; transactions for consume-produce money pathsRequire idempotency review for every side-effect consumer
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
grep -E 'Revoked|Assigned|JoinGroup|SyncGroup|CommitFailed|Heartbeat failed' /va...Why the Coordinator Revokes First and Asks Later
awk '/poll-start/{s=$NF} /poll-end/{print $NF-s}' /var/log/app/consumer.log | so...Size the Poll Loop
grep -c 'Heartbeat failed' /var/log/app/consumer.logHeartbeats vs Processing
kubectl rollout restart statefulset orders-consumerStatic Membership and Cooperative Rebalancing for Calm Deplo
kafka-consumer-groups --bootstrap-server $BROKERS --describe --group orders | aw...Idempotent Handlers
kafka-transactions --bootstrap-server $BROKERS --describe --all 2>/dev/null | he...Exactly-Once With Idempotent Producers and Transactions

Key takeaways

1
Failed commit means revoked partitions
stop and re-poll, never continue processing.
2
Batch time must fit in max.poll.interval.ms with 2x margin measured under real latency.
3
Heartbeats and polls are separate liveness channels with separate failures and fixes.
4
Static membership plus cooperativeSticky deletes deploy-driven rebalance storms.
5
Atomic idempotency guards make guaranteed replays harmless; commits only shrink them.
6
Transactions serve money-path consume-produce flows; handlers cover everything else.

Common mistakes to avoid

5 patterns
×

Catching CommitFailedException and continuing to process

Symptom
Duplicates explode because the failed commit's partitions now belong to another consumer replaying the same records.
Fix
Treat the exception as lost ownership: stop, re-poll, and let assignment settle — plus idempotent handlers so the inevitable replay is harmless.
×

Load-testing consumers against mocked dependencies

Symptom
50ms mocks hide 40x production latency; batch arithmetic passes in staging and rebalances hourly in production.
Fix
Inject production-like (p99-plus) dependency latency in consumer load tests and demand 2x interval margin at batch p99.
×

Raising max.poll.interval.ms instead of fixing batch size

Symptom
Rebalances get rarer but dead consumers take longer to detect, and the underlying 17-minute batches still hog partitions.
Fix
Cut max.poll.records and cap downstream calls first; extend the interval only for genuinely slow work with eyes open on detection delay.
×

Leaving auto-commit on for handlers with side effects

Symptom
Timer commits fire mid-batch, replay boundaries go unpredictable, and duplicates bypass even careful commitSync logic.
Fix
Disable auto-commit for side-effect handlers; commitSync per guarded batch with revoke-hook commits to shrink replay windows.
×

Reusing or randomizing group.instance.id across instances

Symptom
Duplicate IDs fence live members out; random IDs rejoin as strangers — either way, chronic rebalances with confusing logs.
Fix
Derive the ID from stable unique identity (StatefulSet pod name or persisted UID), one value per live instance, forever.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Your consumer throws CommitFailedException. What just happened?
Q02SENIOR
How do you prove slow processing versus heartbeat starvation?
Q03SENIOR
Why do rebalances cause duplicates, and what's the mandatory defense?
Q04SENIOR
What do static membership and cooperativeSticky each contribute?
Q05SENIOR
When do Kafka transactions earn their complexity?
Q01 of 05JUNIOR

Your consumer throws CommitFailedException. What just happened?

ANSWER
The group rebalanced and revoked the consumer's partitions before it committed — usually because processing exceeded max.poll.interval.ms or heartbeats failed past session.timeout.ms. The late commit was correctly rejected since a new owner holds those partitions. Fix the liveness cause, never swallow the exception and continue.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Should I catch CommitFailedException and retry the commit?
02
Why does my consumer rebalance with no deploys or failures?
03
What's a safe max.poll.records value?
04
Do I need Kafka transactions for exactly-once?
05
Why do rebalances spike right after a deploy?
06
How do I stop duplicates without transactions?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Cloud. Mark it forged?

5 min read · try the examples if you haven't

Previous
AWS AccessDenied Authorization Fix
13 / 13 · Cloud