Kafka Consumers In Java
Purpose
Kafka stores an ordered log per partition and consumer groups layer competing-consumer
semantics over it. Consumption does not delete a record; retention/compaction controls its
lifetime, while each group stores offsets independently and can seek or reset. Additional
groups do not advance one another's offsets, but they do add broker fetch, cache, network and
downstream load. Treat the committed offset as a recovery checkpoint, not proof that a
business effect happened.
Two decisions follow. Where the commit sits relative to the work decides the guarantee (the
vocabulary is delivery-semantics; do not re-derive it here). How long the handler takes
between polls affects membership, and reassignment can replay work beyond its recovery
checkpoint. The failure this prevents is the consumer that reprocesses a batch
every few minutes under load with no error, no retry and no broker fault: a slow handler trips
the poll interval, the member is evicted, the group rebalances, the batch comes back.
Workflow
First inspect the resolved Kafka client, Java toolchain, broker version, group.protocol,
assignment mode (subscribe versus manual assign) and any Spring container configuration.
References use Kafka 4.1 API semantics; snippets are partial, not a runnable application or
authorization to upgrade. Manual assignment does not participate in group rebalances.
- Fix the guarantee first — where the commit sits relative to the side effect.
delivery-semantics owns the answer; everything below assumes at-least-once plus a
repeat-safe handler (idempotency).
- Choose synchronous processing, manual offset tracking or a framework-managed ack mode.
Auto-commit can be at-least-once only when every record from the previous
poll() finishes
before the next poll()/close; disable it for asynchronous work or when the exact commit
boundary must be explicit.
- Measure the whole poll-cycle tail.
records returned × per-record time is a conservative
estimate only for serial homogeneous work; include deserialization, queueing, retries,
commits, batch overhead and correlated dependency latency against max.poll.interval.ms.
- Choose the assignment strategy and membership shape — incremental cooperative
assignment, plus static membership if rolling restarts dominate rebalances
(
references/poll-loop-and-rebalance.md).
- Decide reset behavior for the consumer's subscriptions.
auto.offset.reset is a consumer
configuration used when no initial offset exists or its current offset is unavailable.
Different per-topic policies require separate consumers or explicit assignment/seek handling.
- Instrument lag in time, per partition, and alert on that rather than record counts
(
references/offsets-and-lag.md).
- Prove it by fault injection — kill the consumer mid-batch and assert no loss; force a
rebalance under load and assert the downstream outcome.
Report the observed poll/commit/rebalance evidence, the proposed failure mechanism, and the
test that would confirm it. Missing logs or completion tracking leave the diagnosis conditional;
polling regularly is not proof that offloaded work is completing.
Decision block
Process on the poll thread when:
- the measured worst credible poll cycle is comfortably below max.poll.interval.ms
- per-partition ordering must hold end to end and the handler is the last step
- the handler is CPU-bound or calls a dependency with a short, bounded tail
Reduce max.poll.records first when:
- the batch, not the record, is what overruns. One setting, no structural change
Move work off the poll thread with pause/resume when:
- a single record's handler can exceed the poll interval on its own, or the dependency's
latency tail is unbounded or externally controlled
- throughput needs concurrency while the poll thread remains responsive; preserve one ordered
lane per partition or explicitly spend per-partition ordering, and track the highest
contiguous completed offset rather than the maximum completion
Hand the work to a task queue instead when:
- the unit takes minutes or must survive independently of the consumer, and per-key
ordering is not required (task-queues-and-competing-consumers)
Raise max.poll.interval.ms when:
- legitimate bounded processing cannot fit after batch/concurrency changes and the slower
detection of a live-but-not-polling member is acceptable; process death is normally found
by the session timeout, with static-membership nuances
Rules
- Consumption removes nothing. Reprocessing is a seek, not recovery of deleted data, and a
second group has independent position but consumes shared broker/downstream resources.
Conversely there is no queue-style deletion drain: retention removes on
time or size whether or not anyone consumed.
- Under a normal group assignment, one topic-partition is assigned to at most one member at a
time. Useful member concurrency is bounded by the assignable partitions across the
subscription and assignor constraints; local handler concurrency is a separate decision.
Extra Spring container consumers may idle and enlarge group coordination overhead.
enable.auto.commit=true periodically commits offsets of records returned by prior polls
as part of consumer polling. If all those records complete synchronously before the next
poll/close, it can be at-least-once. If records escape to asynchronous workers, offsets can
advance before effects complete and crash can lose work. Auto-commit does not remove the
duplicate window.
- Slow application processing normally trips
max.poll.interval.ms; process/network
liveness trips the session timeout. Heartbeats are independent of record handling in the
classic Java client, while the newer consumer group protocol lets the broker control the
heartbeat interval. Static members that exceed the poll interval stop heartbeating and may
retain assignment until session expiry. Check client/broker protocol and version rather
than applying one timing diagram universally.
- The fix for a slow handler is a smaller
max.poll.records, a faster handler, or pause() on
the assigned partitions with the work on a bounded executor while the loop keeps polling
(concurrency-limiting-and-bulkheads). Polling into an unbounded executor only moves the
backlog into the heap.
- A rebalance or crash can redeliver records after the last committed next offset. A graceful
rebalance does not necessarily duplicate every uncommitted record if revocation commits a
safe contiguous position, but correctness cannot depend on that callback during eviction or
process death. Commit granularity is a duplicate-window and broker-load decision.
- Eager rebalancing revokes the current assignment before redistribution; cooperative
rebalancing can retain partitions that need not move.
CooperativeStickyAssignor requires a
compatible staged rollout, and Kafka's newer consumer rebalance protocol changes assignor
configuration/coordination. Select against deployed client and broker versions.
- Set a stable, unique
group.instance.id when ungraceful short restarts dominate and delayed
reassignment is acceptable. Graceful leave, duplicate instance IDs and orchestrator identity
reuse have different behavior; static membership is not a blanket way to eliminate rolling
rebalances. The price is partitions remaining unavailable until session expiry after a dead
member.
auto.offset.reset is a fallback, not an instruction to override a valid position.
New groups, expired checkpoints or offsets outside retention can activate it. latest
skips retained records preceding the resolved end; earliest starts at the affected
partition's retained beginning; none raises an error for explicit recovery handling.
- No single lag number is sufficient. Record lag needs arrival/service rates to estimate
catch-up; timestamp age can be producer-clock skewed, sparse, compacted or based on create
versus append time. Track per-partition next-record age where meaningful, oldest in-flight
age, record/byte lag, arrival and completion rates, and projected catch-up time.
- Consumer shutdown is a drain: stop admission, bound in-flight completion while maintaining
ownership when feasible, commit safe positions, then
close() within the grace budget.
Static membership/protocol can retain assignment after close until expiry; do not promise
immediate reassignment. Sequencing is kubernetes-service-lifecycle.
- Deserialisation runs on the poll thread and bills as consumer cost, not handler cost. A record
that cannot be deserialised can block progress, but schema-service outages or configuration
errors can be recoverable. Preserve raw bytes/offset and classify before routing or skipping
(
poison-messages-and-dlq); the format's own cost is serialization-performance.
KafkaConsumer is not thread-safe. Keep poll, assignment, pause/resume, seek and commit on
the owning thread; other threads signal it through a thread-safe queue and wakeup(). For
parallel processing, advance only across the completed prefix of delivered records per
partition and ownership epoch. Offsets may have numeric gaps; never wait for nonexistent
records or commit past unfinished delivered records.
References
- The poll loop and the rebalance — the loop's
contract, what each timeout actually bounds, the pause/resume shape for slow work, the
rebalance sequence annotated with where duplicates enter, and the settings that reduce
rebalance pain by role. Read when a group rebalances under load, or before moving work off
the poll thread.
- Offsets and lag — commit strategies compared with the
guarantee and duplicate window each yields,
auto.offset.reset as an explicit decision, lag
in time versus records, and the fault-injection tests that prove no loss under at-least-once.
Read when choosing a commit strategy or building consumer alerts.
1---2name: kafka-consumers-in-java3description: Operating a Kafka consumer from Java: the log-not-a-queue model where consumption removes nothing and position is an offset; the rebalance as the central operational event, with cooperative assignment as the mitigation and where duplicates enter; why slow processing trips max.poll.interval.ms, not the session timeout; pause/resume for slow work; commit strategies; auto.offset.reset as a data-loss-or-reprocessing decision; and lag as record, byte, time and catch-up signals. Use when a group rebalances repeatedly under load, when records are reprocessed after a deploy, when enable.auto.commit is left on, when a consumer starts from the wrong place after an outage. Not ordering scope (message-ordering-and-partitioning), guarantees (delivery-semantics), repeat-safe handlers (idempotency), the record that never succeeds (poison-messages-and-dlq), deserialisation cost (serialization-performance), in-flight bounds (concurrency-limiting-and-bulkheads), or drain (kubernetes-service-lifecycle).4---56# Kafka Consumers In Java78## Purpose910Kafka stores an ordered log per partition and consumer groups layer competing-consumer11semantics over it. Consumption does not delete a record; retention/compaction controls its12lifetime, while each group stores offsets independently and can seek or reset. Additional13groups do not advance one another's offsets, but they do add broker fetch, cache, network and14downstream load. Treat the committed offset as a recovery checkpoint, not proof that a15business effect happened.1617Two decisions follow. **Where the commit sits relative to the work** decides the guarantee (the18vocabulary is `delivery-semantics`; do not re-derive it here). **How long the handler takes19between polls** affects membership, and reassignment can replay work beyond its recovery20checkpoint. The failure this prevents is the consumer that reprocesses a batch21every few minutes under load with no error, no retry and no broker fault: a slow handler trips22the poll interval, the member is evicted, the group rebalances, the batch comes back.2324## Workflow2526First inspect the resolved Kafka client, Java toolchain, broker version, `group.protocol`,27assignment mode (`subscribe` versus manual `assign`) and any Spring container configuration.28References use Kafka 4.1 API semantics; snippets are partial, not a runnable application or29authorization to upgrade. Manual assignment does not participate in group rebalances.30311. **Fix the guarantee first** — where the commit sits relative to the side effect.32 `delivery-semantics` owns the answer; everything below assumes at-least-once plus a33 repeat-safe handler (`idempotency`).342. **Choose synchronous processing, manual offset tracking or a framework-managed ack mode.**35 Auto-commit can be at-least-once only when every record from the previous `poll()` finishes36 before the next `poll()`/close; disable it for asynchronous work or when the exact commit37 boundary must be explicit.383. **Measure the whole poll-cycle tail.** `records returned × per-record time` is a conservative39 estimate only for serial homogeneous work; include deserialization, queueing, retries,40 commits, batch overhead and correlated dependency latency against `max.poll.interval.ms`.414. **Choose the assignment strategy and membership shape** — incremental cooperative42 assignment, plus static membership if rolling restarts dominate rebalances43 (`references/poll-loop-and-rebalance.md`).445. **Decide reset behavior for the consumer's subscriptions.** `auto.offset.reset` is a consumer45 configuration used when no initial offset exists or its current offset is unavailable.46 Different per-topic policies require separate consumers or explicit assignment/seek handling.476. **Instrument lag in time, per partition**, and alert on that rather than record counts48 (`references/offsets-and-lag.md`).497. **Prove it by fault injection** — kill the consumer mid-batch and assert no loss; force a50 rebalance under load and assert the downstream outcome.5152Report the observed poll/commit/rebalance evidence, the proposed failure mechanism, and the53test that would confirm it. Missing logs or completion tracking leave the diagnosis conditional;54polling regularly is not proof that offloaded work is completing.5556## Decision block5758```text59Process on the poll thread when:60- the measured worst credible poll cycle is comfortably below max.poll.interval.ms61- per-partition ordering must hold end to end and the handler is the last step62- the handler is CPU-bound or calls a dependency with a short, bounded tail6364Reduce max.poll.records first when:65- the batch, not the record, is what overruns. One setting, no structural change6667Move work off the poll thread with pause/resume when:68- a single record's handler can exceed the poll interval on its own, or the dependency's69 latency tail is unbounded or externally controlled70- throughput needs concurrency while the poll thread remains responsive; preserve one ordered71 lane per partition or explicitly spend per-partition ordering, and track the highest72 contiguous completed offset rather than the maximum completion7374Hand the work to a task queue instead when:75- the unit takes minutes or must survive independently of the consumer, and per-key76 ordering is not required (task-queues-and-competing-consumers)7778Raise max.poll.interval.ms when:79- legitimate bounded processing cannot fit after batch/concurrency changes and the slower80 detection of a live-but-not-polling member is acceptable; process death is normally found81 by the session timeout, with static-membership nuances82```8384## Rules8586- **Consumption removes nothing.** Reprocessing is a seek, not recovery of deleted data, and a87 second group has independent position but consumes shared broker/downstream resources.88 Conversely there is no queue-style deletion drain: retention removes on89 time or size whether or not anyone consumed.90- Under a normal group assignment, one topic-partition is assigned to at most one member at a91 time. Useful member concurrency is bounded by the assignable partitions across the92 subscription and assignor constraints; local handler concurrency is a separate decision.93 Extra Spring container consumers may idle and enlarge group coordination overhead.94- `enable.auto.commit=true` periodically commits offsets of records returned by prior polls95 as part of consumer polling. If all those records complete synchronously before the next96 poll/close, it can be at-least-once. If records escape to asynchronous workers, offsets can97 advance before effects complete and crash can lose work. Auto-commit does not remove the98 duplicate window.99- **Slow application processing normally trips `max.poll.interval.ms`; process/network100 liveness trips the session timeout.** Heartbeats are independent of record handling in the101 classic Java client, while the newer consumer group protocol lets the broker control the102 heartbeat interval. Static members that exceed the poll interval stop heartbeating and may103 retain assignment until session expiry. Check client/broker protocol and version rather104 than applying one timing diagram universally.105- The fix for a slow handler is a smaller `max.poll.records`, a faster handler, or `pause()` on106 the assigned partitions with the work on a **bounded** executor while the loop keeps polling107 (`concurrency-limiting-and-bulkheads`). Polling into an unbounded executor only moves the108 backlog into the heap.109- A rebalance or crash can redeliver records after the last committed next offset. A graceful110 rebalance does not necessarily duplicate every uncommitted record if revocation commits a111 safe contiguous position, but correctness cannot depend on that callback during eviction or112 process death. Commit granularity is a duplicate-window and broker-load decision.113- Eager rebalancing revokes the current assignment before redistribution; cooperative114 rebalancing can retain partitions that need not move. `CooperativeStickyAssignor` requires a115 compatible staged rollout, and Kafka's newer consumer rebalance protocol changes assignor116 configuration/coordination. Select against deployed client and broker versions.117- Set a stable, unique `group.instance.id` when ungraceful short restarts dominate and delayed118 reassignment is acceptable. Graceful leave, duplicate instance IDs and orchestrator identity119 reuse have different behavior; static membership is not a blanket way to eliminate rolling120 rebalances. The price is partitions remaining unavailable until session expiry after a dead121 member.122- **`auto.offset.reset` is a fallback**, not an instruction to override a valid position.123 New groups, expired checkpoints or offsets outside retention can activate it. `latest`124 skips retained records preceding the resolved end; `earliest` starts at the affected125 partition's retained beginning; `none` raises an error for explicit recovery handling.126- **No single lag number is sufficient.** Record lag needs arrival/service rates to estimate127 catch-up; timestamp age can be producer-clock skewed, sparse, compacted or based on create128 versus append time. Track per-partition next-record age where meaningful, oldest in-flight129 age, record/byte lag, arrival and completion rates, and projected catch-up time.130- Consumer shutdown is a drain: stop admission, bound in-flight completion while maintaining131 ownership when feasible, commit safe positions, then `close()` within the grace budget.132 Static membership/protocol can retain assignment after close until expiry; do not promise133 immediate reassignment. Sequencing is `kubernetes-service-lifecycle`.134- Deserialisation runs on the poll thread and bills as consumer cost, not handler cost. A record135 that cannot be deserialised can block progress, but schema-service outages or configuration136 errors can be recoverable. Preserve raw bytes/offset and classify before routing or skipping137 (`poison-messages-and-dlq`); the format's own cost is `serialization-performance`.138- `KafkaConsumer` is not thread-safe. Keep `poll`, assignment, pause/resume, seek and commit on139 the owning thread; other threads signal it through a thread-safe queue and `wakeup()`. For140 parallel processing, advance only across the completed prefix of delivered records per141 partition and ownership epoch. Offsets may have numeric gaps; never wait for nonexistent142 records or commit past unfinished delivered records.143144## References145146- [The poll loop and the rebalance](references/poll-loop-and-rebalance.md) — the loop's147 contract, what each timeout actually bounds, the pause/resume shape for slow work, the148 rebalance sequence annotated with where duplicates enter, and the settings that reduce149 rebalance pain by role. Read when a group rebalances under load, or before moving work off150 the poll thread.151- [Offsets and lag](references/offsets-and-lag.md) — commit strategies compared with the152 guarantee and duplicate window each yields, `auto.offset.reset` as an explicit decision, lag153 in time versus records, and the fault-injection tests that prove no loss under at-least-once.154 Read when choosing a commit strategy or building consumer alerts.