apache/kafka · error · IllegalStateException
Cannot send offsets if a transaction is not in progress (cur
Error message
Cannot send offsets if a transaction is not in progress (currentState= {currentState}) What it means
Thrown by TransactionManager.sendOffsetsToTransaction when the current state is anything other than IN_TRANSACTION. Sending consumer offsets to a transaction (the consume-transform-produce pattern) is only legal after beginTransaction() and before commit/abort; outside that window the coordinator would reject it, so the client rejects it up front with an IllegalStateException that leaves producer state intact.
Source
Thrown at clients/src/main/java/org/apache/kafka/clients/producer/internals/TransactionManager.java:439
enqueueRequest(handler);
// If an epoch bump is required for recovery, initialize the transaction after completing the EndTxn request.
// If we are upgrading to TV2 transactions on the next transaction, also bump the epoch.
if (clientSideEpochBumpRequired) {
return initializeTransactions(this.producerIdAndEpoch);
}
return handler.result;
}
public synchronized TransactionalRequestResult sendOffsetsToTransaction(final Map<TopicPartition, OffsetAndMetadata> offsets,
final ConsumerGroupMetadata groupMetadata) {
ensureTransactional();
throwIfPendingState(TransactionOperation.SEND_OFFSETS_TO_TRANSACTION);
maybeFailWithError();
if (currentState != State.IN_TRANSACTION) {
throw new IllegalStateException("Cannot send offsets if a transaction is not in progress " +
"(currentState= " + currentState + ")");
}
// In transaction V2, the client will skip sending AddOffsetsToTxn before sending txnOffsetCommit.
TxnRequestHandler handler;
if (isTransactionV2Enabled()) {
log.debug("Begin adding offsets {} for consumer group {} to transaction with transaction protocol V2", offsets, groupMetadata);
handler = txnOffsetCommitHandler(null, offsets, groupMetadata);
transactionStarted = true;
} else {
log.debug("Begin adding offsets {} for consumer group {} to transaction", offsets, groupMetadata);
AddOffsetsToTxnRequest.Builder builder = new AddOffsetsToTxnRequest.Builder(
new AddOffsetsToTxnRequestData()
.setTransactionalId(transactionalId)
.setProducerId(producerIdAndEpoch.producerId)
.setProducerEpoch(producerIdAndEpoch.epoch)
.setGroupId(groupMetadata.groupId())
);View on GitHub (pinned to c31c9215e1)
Solutions
- Call producer.beginTransaction() before producer.sendOffsetsToTransaction(...).
- Ensure the offset send happens within the begin..commit bracket: initTransactions -> beginTransaction -> (send + sendOffsetsToTransaction) -> commitTransaction.
- After an abort, start a fresh transaction (beginTransaction) before sending offsets again — the state is READY, not IN_TRANSACTION.
- Add a state assertion helper in your code so mis-ordered lifecycle calls fail loudly in tests.
Example fix
// before producer.initTransactions(); producer.sendOffsetsToTransaction(offsets, groupMetadata); // throws producer.commitTransaction(); // after producer.initTransactions(); producer.beginTransaction(); producer.sendOffsetsToTransaction(offsets, groupMetadata); producer.commitTransaction();
Defensive patterns
Strategy: validation
Validate before calling
// KafkaProducer hides transaction state; mirror it in your own variable.
private volatile boolean inTransaction = false;
// set true after beginTransaction(), false after commit/abort returns
...
if (!inTransaction) throw new IllegalStateException("sendOffsetsToTransaction requires an open transaction");
producer.sendOffsetsToTransaction(offsets, groupMetadata); Try / catch
try {
producer.sendOffsetsToTransaction(offsets, groupMetadata);
} catch (IllegalStateException ise) {
// transaction lifecycle out of sync; either beginTransaction first or skip this commit
} Prevention
- Always call sendOffsetsToTransaction inside the [beginTransaction, commitTransaction] window.
- Wrap consume-process-produce in a helper that pairs begin/commit/abort with explicit state tracking.
- For exactly-once pipelines, gate on consumer group membership and transaction state before committing offsets.
When it happens
Trigger: Calling producer.sendOffsetsToTransaction(offsets, groupMetadata) before beginTransaction(); after commitTransaction()/abortTransaction() has already completed; or in the READY state because initTransactions() finished but beginTransaction() was skipped. The state-machine guard at line 438-441 fires.
Common situations: Streams-style consume-produce code where the begin/commit bracket is missing or reordered; refactors that moved beginTransaction() into a conditional branch; recovering from an abort and forgetting to begin again before the next offset commit; unit tests that exercise the offset-send path without the full lifecycle.
Related errors
- Cannot add partition {topicPartition} to transaction before
- Cannot add partition {topicPartition} to transaction while i
- Transactional method invoked on a non-transactional producer
- Producer with transactionalId '{transactionalId}' and {produ
- Cannot set a transactional.id without also enabling idempote
AI-assisted analysis of apache/kafka@c31c9215e1 (2026-08-03).
Data as JSON: /data/errors/63fa35da5a6570fa.json.
Report an issue: GitHub.