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:
Mqtt5AsyncClient client = Mqtt5Client.builder() .identifier("my-sensor") .serverHost("broker.hivemq.com") .buildAsync();
// This felt too complex for simple publishingclient.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.
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:
+-----------+ +-----------+| 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
// WRONG - String won't work directlypublisher.publishWith() .topic("sensors/temp") .payload("24.5") // Compilation error! .send();
// CORRECT - Convert to bytespublisher.publishWith() .topic("sensors/temp") .payload("24.5".getBytes(StandardCharsets.UTF_8)) .send();Mistake 2: Publishing before connecting
// WRONG - Not connected yetpublisher.publishWith() .topic("sensors/temp") .payload(data.getBytes()) .send(); // Throws exception!
// CORRECT - Connect firstpublisher.connect();publisher.publishWith() .topic("sensors/temp") .payload(data.getBytes()) .send();Mistake 3: Using wrong topic format
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:
// 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 deliverypublisher.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 deliverypublisher.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:
// Publish retained message - broker keeps thispublisher.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 disconnectedThis 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:
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 immediatelyFor 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.
Related Concepts
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
- Use blocking client for simple publishing: When you just need to push messages without complex async flows
- Always convert payloads to bytes: The API requires
byte[]payloads - Connect before publishing: Obvious but easy to forget during refactoring
- Choose appropriate QoS: Balance reliability vs. performance
- 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