Skip to content

How to Publish MQTT Messages in Java with HiveMQ Client

I needed to send sensor data from my Java application to an MQTT broker, but I kept getting confused about which client API to use and how to properly publish messages. The documentation showed both blocking and async APIs, and I wasn’t sure which one fit my use case.

The Problem

My IoT application needed to publish temperature readings to an MQTT broker every few seconds. I tried using the async API first, but it felt overkill for my simple fire-and-forget publishing scenario. I didn’t need to subscribe to topics or react to incoming messages - I just needed to push data.

Here’s what I tried initially:

FailedAttempt.java
Mqtt5AsyncClient client = Mqtt5Client.builder()
.identifier("my-sensor")
.serverHost("broker.hivemq.com")
.buildAsync();
// This felt too complex for simple publishing
client.connect().thenAccept(connAck -> {
client.publishWith()
.topic("sensors/temp")
.payload("24.5".getBytes())
.send();
});

The async approach worked, but I found myself adding .thenAccept() chains for operations that should be straightforward. I realized I was making this harder than it needed to be.

The Solution: Blocking Client

After reading the HiveMQ documentation more carefully, I discovered the blocking client API. This was exactly what I needed - synchronous operations that execute in order and block until complete.

SimplePublisher.java
import com.hivemq.client.mqtt.MqttClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public class SimplePublisher {
public static void main(String[] args) {
// Create blocking client
Mqtt5BlockingClient publisher = Mqtt5Client.builder()
.identifier("publisher-" + UUID.randomUUID())
.serverHost("broker.hivemq.com")
.serverPort(1883)
.buildBlocking();
// Connect synchronously - blocks until connected
publisher.connect();
// Publish message - blocks until broker confirms
String topic = "sensors/temperature";
String payload = "Hello from Java";
publisher.publishWith()
.topic(topic)
.payload(payload.getBytes(StandardCharsets.UTF_8))
.send();
// Disconnect cleanly
publisher.disconnect();
}
}

This felt much more natural for my use case. Each operation completes before moving to the next line.

Understanding the Flow

The blocking client follows a simple request-response pattern:

Publish Flow Diagram
+-----------+ +-----------+
| Publisher | | Broker |
+-----------+ +-----------+
| |
| 1. connect() |
|-------------------------------->|
| |
| 2. CONNACK (blocks until) |
|<--------------------------------|
| |
| 3. publishWith()...send() |
|-------------------------------->|
| |
| 4. PUBACK (blocks until) |
|<--------------------------------|
| |
| 5. disconnect() |
|-------------------------------->|
| |

The key insight is that each method blocks until the broker responds. This makes the code easy to reason about - I know the message was published before my code moves to the next line.

Common Mistakes I Made

Mistake 1: Forgetting to convert payload to bytes

WrongPayload.java
// WRONG - String won't work directly
publisher.publishWith()
.topic("sensors/temp")
.payload("24.5") // Compilation error!
.send();
// CORRECT - Convert to bytes
publisher.publishWith()
.topic("sensors/temp")
.payload("24.5".getBytes(StandardCharsets.UTF_8))
.send();

Mistake 2: Publishing before connecting

WrongOrder.java
// WRONG - Not connected yet
publisher.publishWith()
.topic("sensors/temp")
.payload(data.getBytes())
.send(); // Throws exception!
// CORRECT - Connect first
publisher.connect();
publisher.publishWith()
.topic("sensors/temp")
.payload(data.getBytes())
.send();

Mistake 3: Using wrong topic format

Topic Format Examples
Valid MQTT topics:
- sensors/temperature/living-room
- home/lights/kitchen
- device/001/status
Invalid (don't use these):
- sensors.temperature (dots not standard)
- sensors\\temperature (wrong separator)
- sensors//temperature (empty level)

Quality of Service Levels

I also learned about QoS levels, which control message delivery guarantees:

QoSExamples.java
// QoS 0 - Fire and forget (default)
publisher.publishWith()
.topic("sensors/temp")
.payload("24.5".getBytes(StandardCharsets.UTF_8))
.qos(MqttQos.AT_MOST_ONCE) // Message might be lost
.send();
// QoS 1 - At least once delivery
publisher.publishWith()
.topic("sensors/temp")
.payload("24.5".getBytes(StandardCharsets.UTF_8))
.qos(MqttQos.AT_LEAST_ONCE) // Message delivered at least once
.send();
// QoS 2 - Exactly once delivery
publisher.publishWith()
.topic("critical/alert")
.payload("OVERHEAT".getBytes(StandardCharsets.UTF_8))
.qos(MqttQos.EXACTLY_ONCE) // Guaranteed exactly once
.send();

For my temperature sensor data, QoS 0 is fine - if one message is lost, the next reading will come in a few seconds anyway. But for critical alerts, QoS 2 ensures the message gets through.

Retained Messages

Another useful feature I discovered is retained messages. The broker stores the last retained message for each topic and delivers it to new subscribers:

RetainedMessage.java
// Publish retained message - broker keeps this
publisher.publishWith()
.topic("device/status")
.payload("online".getBytes(StandardCharsets.UTF_8))
.retain(true) // Broker retains for new subscribers
.send();
// When a new subscriber joins, they immediately receive "online"
// even if the publisher has already disconnected

This is perfect for device status messages - new subscribers immediately know the current state without waiting for the next publish.

When to Use Blocking vs Async

I realized the blocking client is ideal when:

  • Fire-and-forget publishing: You just need to send data
  • Simple workflows: Linear operations without complex dependencies
  • Short-lived applications: Scripts that publish and exit
  • Testing and debugging: Easier to trace execution

The async client is better when:

  • Bidirectional communication: You need to publish and subscribe
  • High throughput: Publishing thousands of messages per second
  • Reactive patterns: Responding to messages or connection events
  • Non-blocking requirements: Can’t block the main thread

Performance Considerations

For high-volume publishing, the blocking client can become a bottleneck. Each publish waits for network round-trip to the broker. In my testing with a local broker:

Publishing Performance
Blocking client (synchronous):
- ~100 messages/second to local broker
- Each publish ~10ms latency
Async client (asynchronous):
- ~10,000 messages/second to local broker
- Publish returns immediately

For my temperature sensor publishing every 5 seconds, the blocking client is perfectly adequate. But if I were building a real-time telemetry system with hundreds of sensors, I’d need the async API.

MQTT Topics: Topics use / as a separator and support wildcards. sensors/+/temperature matches sensors/living-room/temperature but not sensors/living-room/humidity.

Message Expiry: MQTT 5.0 supports message expiry intervals. Messages that can’t be delivered within the expiry period are discarded.

Last Will and Testament (LWT): You can configure a message the broker publishes if your client disconnects unexpectedly.

Key Takeaways

  1. Use blocking client for simple publishing: When you just need to push messages without complex async flows
  2. Always convert payloads to bytes: The API requires byte[] payloads
  3. Connect before publishing: Obvious but easy to forget during refactoring
  4. Choose appropriate QoS: Balance reliability vs. performance
  5. Clean up resources: Disconnect when done, especially in short-lived applications

The HiveMQ blocking client made my IoT publishing straightforward. No callback hell, no complex promise chains - just sequential code that’s easy to understand and maintain.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments