Skip to content

HiveMQ MQTT Client: Blocking vs Async vs Reactive API - Which Should You Use

I needed to add MQTT support to my Java application, and I chose HiveMQ MQTT Client because it seemed well-maintained. But then I hit a wall: the library offers three different API styles—blocking, async, and reactive. Which one should I use?

The documentation explains each style, but it doesn’t clearly say when to use which. After experimenting with all three, I finally understood the trade-offs. Let me share what I learned.

The Problem: Three APIs, No Clear Guidance

When I first looked at the HiveMQ MQTT Client, I saw this in the documentation:

  • buildBlocking() — synchronous operations
  • buildAsync() — asynchronous with CompletableFuture
  • buildReactive() — reactive streams with backpressure

All three connect to the same MQTT broker and do the same things. But which one fits my use case?

Understanding the Three API Styles

1. Blocking API: The Straightforward Approach

The blocking API is the simplest. Every operation blocks the current thread until it completes.

BlockingPublisher.java
Mqtt5BlockingClient client = Mqtt5Client.builder()
.identifier("pub-" + UUID.randomUUID())
.serverHost("broker.hivemq.com")
.serverPort(1883)
.buildBlocking();
// This blocks until connection succeeds
client.connect();
// This blocks until the message is sent
client.publishWith()
.topic("test/topic")
.payload("hello".getBytes())
.send();
// Clean disconnect (also blocks)
client.disconnect();

When to use it:

  • Simple publish-only scenarios
  • Command-line tools or scripts
  • When you don’t need to handle incoming messages
  • When blocking a thread is acceptable (dedicated thread pools)

When NOT to use it:

  • In reactive applications (Spring WebFlux, etc.)
  • In high-throughput scenarios where thread blocking hurts performance
  • When you need to subscribe to topics

The blocking client “works well when we don’t need to react to incoming messages,” according to the documentation. This was my first clue: if I need to subscribe and receive messages, blocking isn’t the right choice.

2. Async API: Event-Driven Without the Complexity

The async API uses CompletableFuture and callbacks. It’s non-blocking but doesn’t require learning a reactive framework.

AsyncSubscriber.java
Mqtt5AsyncClient client = Mqtt5Client.builder()
.identifier("sub-" + UUID.randomUUID())
.serverHost("broker.hivemq.com")
.serverPort(1883)
.buildAsync();
// Connect returns a CompletableFuture
client.connect().thenAccept(connAck -> {
System.out.println("Connected: " + connAck.getReasonCode());
// Subscribe after connection
client.subscribeWith()
.topicFilter("test/#")
.send()
.thenAccept(subAck -> {
System.out.println("Subscribed successfully");
});
});
// Set up callback for incoming messages
client.publishes(MqttGlobalPublishFilter.SUBSCRIBED, publish -> {
System.out.println("Received message on topic: " + publish.getTopic());
System.out.println("Payload: " + new String(publish.getPayloadAsBytes()));
});

I made a mistake here initially. I tried to use .join() on every CompletableFuture to wait for completion, essentially turning async into blocking:

AntiPattern.java
// DON'T DO THIS - defeats the purpose of async
client.connect().join(); // Blocks!
client.subscribeWith().topicFilter("test/#").send().join(); // Blocks!

This defeats the purpose of the async API. Instead, chain operations with .thenCompose() or .thenAccept():

ProperAsync.java
client.connect()
.thenCompose(conn -> client.subscribeWith()
.topicFilter("test/#")
.send())
.thenAccept(subAck -> System.out.println("Ready to receive messages"))
.exceptionally(ex -> {
System.err.println("Failed: " + ex.getMessage());
return null;
});

When to use it:

  • Subscribing to topics and handling incoming messages
  • Event-driven applications
  • When you want non-blocking without reactive frameworks
  • Spring MVC or traditional Java applications

3. Reactive API: For Stream Processing

The reactive API is built on Reactive Streams (like Project Reactor). It’s ideal for high-throughput scenarios with backpressure support.

ReactiveStreams.java
Mqtt5RxClient client = Mqtt5Client.builder()
.identifier("rx-" + UUID.randomUUID())
.serverHost("broker.hivemq.com")
.serverPort(1883)
.buildRx();
// Connect returns a Single<Mqtt5ConnAck>
client.connect()
.flatMap(connAck -> client.subscribeStream()
.topicFilter("sensors/#")
.addSubscription()
.execute()) // Returns Flowable<Mqtt5Publish>
.flatMap(publish -> {
// Process each message as a stream
return processMessage(publish);
})
.subscribe();

The key advantage is backpressure. If your message processing can’t keep up with the incoming rate, the reactive API signals the broker to slow down.

When to use it:

  • High-throughput stream processing
  • Reactive applications (Spring WebFlux, Micronaut)
  • When you need backpressure control
  • Complex stream transformations

Decision Guide: Which API to Choose?

After experimenting, I created this decision tree:

API Selection Flow
┌─────────────────────────┐
│ Do you need to receive │
│ incoming messages? │
└───────────┬─────────────┘
┌───────────┴───────────┐
│ │
NO YES
│ │
▼ ▼
┌───────────────────┐ ┌─────────────────────────┐
│ Use BLOCKING │ │ Is your app reactive? │
│ (simplest) │ └───────────┬─────────────┘
└───────────────────┘ │
┌───────────┴───────────┐
│ │
NO YES
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Use ASYNC │ │ Use REACTIVE │
│ (event-driven) │ │ (backpressure) │
└───────────────────┘ └───────────────────┘

Practical Example: Combining Approaches

In my project, I needed both: a simple publisher and a message receiver. I used different clients for each:

MixedUsage.java
// Publisher: Simple, infrequent, one-way
Mqtt5BlockingClient publisher = Mqtt5Client.builder()
.identifier("publisher-" + UUID.randomUUID())
.serverHost(brokerHost)
.buildBlocking();
// Subscriber: Needs to react to incoming messages
Mqtt5AsyncClient subscriber = Mqtt5Client.builder()
.identifier("subscriber-" + UUID.randomUUID())
.serverHost(brokerHost)
.buildAsync();
// Publisher connects, sends, disconnects
publisher.connect();
publisher.publishWith()
.topic("status")
.payload("online".getBytes())
.send();
publisher.disconnect();
// Subscriber stays connected and handles messages
subscriber.connect().join();
subscriber.publishes(MqttGlobalPublishFilter.SUBSCRIBED, msg -> {
handleIncomingMessage(msg);
});

Common Mistakes I Made

Mistake 1: Using Blocking API for Subscriptions

I initially tried to use the blocking API for receiving messages. It doesn’t work well because blocking operations don’t fit an event-driven model.

WontWork.java
// Blocking client cannot handle incoming messages properly
Mqtt5BlockingClient client = ...;
client.connect();
// There's no good way to wait for messages with blocking client

Mistake 2: Not Handling CompletableFuture Exceptions

With the async API, I forgot to handle exceptions:

MissingExceptionHandling.java
// Bad: Exceptions silently fail
client.connect()
.thenCompose(c -> client.subscribeWith().topicFilter("test").send());
// If this fails, you'll never know
// Good: Always handle exceptions
client.connect()
.thenCompose(c -> client.subscribeWith().topicFilter("test").send())
.exceptionally(ex -> {
log.error("Connection failed", ex);
return null;
});

Mistake 3: Mixing API Styles in Reactive Applications

In a Spring WebFlux application, I tried using the async API with .join() everywhere. This blocked the reactor threads and caused performance issues. The reactive API is the right choice here.

Summary

API StyleBest ForThread BehaviorComplexity
BlockingPublish-only, CLI toolsBlocks threadLow
AsyncSubscriptions, event-drivenNon-blocking with callbacksMedium
ReactiveHigh-throughput, Spring WebFluxNon-blocking with backpressureHigher

The key insight: Your use case determines the API, not the other way around.

  • If you only publish, use blocking.
  • If you subscribe and react, use async.
  • If you process streams at scale, use reactive.

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