apache/rocketmq · critical · MQClientException

send message Exception

Error message

send message Exception

What it means

Thrown by sendMessageInTransaction when the initial synchronous send of the half message (this.send(msg) with PROPERTY_TRANSACTION_PREPARED set) raises any Exception. Without a successfully delivered half message there is no transaction to execute, so the client surfaces the underlying send failure wrapped in MQClientException('send message Exception', cause).

Source

Thrown at client/src/main/java/org/apache/rocketmq/client/impl/producer/DefaultMQProducerImpl.java:1450

    public TransactionSendResult sendMessageInTransaction(final Message msg,
        final TransactionListener localTransactionListener, final Object arg)
        throws MQClientException {
        TransactionListener transactionListener = getCheckListener();
        if (null == localTransactionListener && null == transactionListener) {
            throw new MQClientException("tranExecutor is null", null);
        }

        ensureNotDelayedForTransactional(msg);
        Validators.checkMessage(msg, this.defaultMQProducer);

        SendResult sendResult = null;
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
        try {
            sendResult = this.send(msg);
        } catch (Exception e) {
            throw new MQClientException("send message Exception", e);
        }

        LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
        Throwable localException = null;
        switch (sendResult.getSendStatus()) {
            case SEND_OK: {
                try {
                    if (sendResult.getTransactionId() != null) {
                        msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
                    }
                    String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                    if (null != transactionId && !"".equals(transactionId)) {
                        msg.setTransactionId(transactionId);
                    }
                    if (null != localTransactionListener) {
                        localTransactionState = localTransactionListener.executeLocalTransaction(msg, arg);
                    } else {
                        log.debug("Used new transaction API");

View on GitHub (pinned to 293f588571)

Solutions

  1. Inspect the nested cause: MQClientException with 'No route info' -> create/verify the topic; RemotingTimeoutException -> check broker health/latency; MQBrokerException -> act on response code.
  2. Verify the topic exists and the producing permission is set before entering the transactional flow (fetchPublishMessageQueues as a readiness probe).
  3. Keep the local transaction unstarted until sendMessageInTransaction returns, so a failed half send does not orphan a local DB transaction.
  4. Retry the whole sendMessageInTransaction call idempotently for transient network causes.

Example fix

// before
try {
    producer.sendMessageInTransaction(msg, listener, arg);
} catch (MQClientException e) {
    // local DB tx already committed -> inconsistent!
}

// after
// start nothing locally beforehand; RocketMQ drives the local branch via listener
TransactionSendResult r = producer.sendMessageInTransaction(msg, listener, arg);
if (r.getLocalTransactionState() != LocalTransactionState.COMMIT_MESSAGE) {
    metrics.txAborted(arg);
}
Defensive patterns

Strategy: retry

Validate before calling

// probe route before entering transactional flow
if (producer.fetchPublishMessageQueues(msg.getTopic()).isEmpty()) {
    throw new IllegalStateException("topic " + msg.getTopic() + " has no route; half message would fail");
}

Try / catch

try {
    TransactionSendResult r = producer.sendMessageInTransaction(msg, listener, arg);
} catch (MQClientException e) {
    if (e.getMessage().startsWith("send message Exception")) {
        Throwable c = e.getCause();
        if (isTransient(c)) retryTransactionally(msg, listener, arg); // half send never landed
        else throw new IllegalStateException("half message send failed", c);
    } else throw e;
}

Prevention

When it happens

Trigger: producer.sendMessageInTransaction(...) where the half-message send fails: no route for the topic, broker rejection, remoting timeout, InterruptedException during shutdown, or message validation failure.

Common situations: Transactional topic not created / no route (most common on fresh clusters); broker down or busy at the moment of the half send; sending during producer shutdown (interrupt); message body violating broker limits.

Related errors


AI-assisted analysis of apache/rocketmq@293f588571 (2026-08-14). Data as JSON: /api/errors/350148f847231a10. Report an issue: GitHub.