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

  1. Call producer.beginTransaction() before producer.sendOffsetsToTransaction(...).
  2. Ensure the offset send happens within the begin..commit bracket: initTransactions -> beginTransaction -> (send + sendOffsetsToTransaction) -> commitTransaction.
  3. After an abort, start a fresh transaction (beginTransaction) before sending offsets again — the state is READY, not IN_TRANSACTION.
  4. 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

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


AI-assisted analysis of apache/kafka@c31c9215e1 (2026-08-03). Data as JSON: /data/errors/63fa35da5a6570fa.json. Report an issue: GitHub.