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
- Inspect the nested cause: MQClientException with 'No route info' -> create/verify the topic; RemotingTimeoutException -> check broker health/latency; MQBrokerException -> act on response code.
- Verify the topic exists and the producing permission is set before entering the transactional flow (fetchPublishMessageQueues as a readiness probe).
- Keep the local transaction unstarted until sendMessageInTransaction returns, so a failed half send does not orphan a local DB transaction.
- 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
- Start local DB work only inside the TransactionListener, never before sendMessageInTransaction.
- Pre-verify the transactional topic's route and permissions during deployment checks.
- Make half-message retries idempotent (unique business key) since a timeout may have actually delivered.
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
- tranExecutor is null
- Transactional messages do not support delayed delivery
- send request message to <{}> fail
- 13
- Sending message to topic[%s] is forbidden.
AI-assisted analysis of apache/rocketmq@293f588571 (2026-08-14).
Data as JSON: /api/errors/350148f847231a10.
Report an issue: GitHub.