IoT Tutorials

Java MQTT Client Tutorial: Eclipse Paho vs HiveMQ Client with TLS and MQTT 5

Compare the two main Java MQTT libraries, Eclipse Paho and the HiveMQ MQTT Client, and implement publish/subscribe over TLS with MQTT 5 using each of them.

Java coffee cup icon beside a broker diagram illustrating a java mqtt client over TLS
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Choosing a Java MQTT client usually comes down to two libraries: Eclipse Paho and the HiveMQ MQTT Client. This tutorial compares them honestly, then implements the same publish/subscribe program with each, using MQTT 5 over TLS on port 8883, automatic reconnects and a Last Will, so you can pick the one that suits your codebase.

Java MQTT Client Libraries Compared

Both libraries are open source, both speak MQTT 5, and both are used in production. They differ mainly in API style and in how actively they are developed.

Eclipse Paho Java HiveMQ MQTT Client
Maven artifact org.eclipse.paho.mqttv5.client (MQTT 5), org.eclipse.paho.client.mqttv3 (3.1.1) com.hivemq:hivemq-mqtt-client
MQTT versions Separate artifacts for 3.1.1 and 5 3.1.1 and 5 in one library
API style Classic callbacks, blocking or async with tokens Fluent builders; blocking, async (CompletableFuture) and reactive (RxJava)
Networking Plain Java sockets Netty
Automatic reconnect Option on connection options Configurable on the client builder
Dependencies None beyond the JDK Netty, RxJava and a few others
Release cadence Infrequent in recent years More active

A practical rule: Paho is small, dependency-free and familiar from countless examples, which makes it attractive for Android or tight environments. The HiveMQ client has a more modern API and handles back-pressure well under high message rates, which suits backend services. Spring developers often use Paho indirectly through Spring Integration; see the Spring Boot MQTT guide for that route.

Project Setup

Both examples use Java 17 and Maven. Add one or both dependencies to pom.xml:

<dependencies>
  <dependency>
    <groupId>org.eclipse.paho</groupId>
    <artifactId>org.eclipse.paho.mqttv5.client</artifactId>
    <version>1.2.5</version>
  </dependency>
  <dependency>
    <groupId>com.hivemq</groupId>
    <artifactId>hivemq-mqtt-client</artifactId>
    <version>1.3.3</version>
  </dependency>
</dependencies>

For testing, connect to the public MQTT test broker at broker.connectmq.com. It accepts anonymous clients with any non-empty, unique client ID. Use a random prefix such as connectmq/demo/j6t1ve and keep in mind that public topics can be read by anyone.

Java's default trust store includes ISRG Root X1, the Let's Encrypt root used by the broker, on all current JDK releases. No keystore setup is needed for TLS.

Example 1: Eclipse Paho MQTT 5 Client

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.eclipse.paho.mqttv5.client.IMqttToken;
import org.eclipse.paho.mqttv5.client.MqttCallback;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse;
import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;

public class PahoDemo {

    static String env(String key, String def) {
        String v = System.getenv(key);
        return (v == null || v.isEmpty()) ? def : v;
    }

    public static void main(String[] args) throws Exception {
        String serverUri = env("MQTT_URL", "ssl://broker.connectmq.com:8883");
        String clientId = env("MQTT_CLIENT_ID", "paho-j6t1ve");
        String prefix = env("MQTT_PREFIX", "connectmq/demo/j6t1ve");
        String user = System.getenv("MQTT_USERNAME");
        String commandFilter = prefix + "/cmd/#";
        ExecutorService worker = Executors.newSingleThreadExecutor();

        MqttClient client = new MqttClient(serverUri, clientId, new MemoryPersistence());

        MqttConnectionOptions options = new MqttConnectionOptions();
        options.setAutomaticReconnect(true);
        options.setCleanStart(false);
        options.setSessionExpiryInterval(600L);
        options.setKeepAliveInterval(30);
        if (user != null) {
            options.setUserName(user);
            options.setPassword(env("MQTT_PASSWORD", "").getBytes(StandardCharsets.UTF_8));
        }
        MqttMessage will = new MqttMessage("offline".getBytes(StandardCharsets.UTF_8));
        will.setQos(1);
        will.setRetained(true);
        options.setWill(prefix + "/status", will);

        client.setCallback(new MqttCallback() {
            @Override
            public void connectComplete(boolean reconnect, String uri) {
                System.out.println((reconnect ? "reconnected to " : "connected to ") + uri);
                if (reconnect) {
                    // Never block the callback thread; resubscribe on a worker.
                    worker.submit(() -> {
                        try {
                            client.subscribe(commandFilter, 1);
                        } catch (MqttException e) {
                            System.err.println("resubscribe failed: " + e.getMessage());
                        }
                    });
                }
            }

            @Override
            public void messageArrived(String topic, MqttMessage message) {
                System.out.println("command on " + topic + ": "
                        + new String(message.getPayload(), StandardCharsets.UTF_8));
            }

            @Override
            public void disconnected(MqttDisconnectResponse response) {
                System.out.println("disconnected: " + response.getReasonString());
            }

            @Override
            public void mqttErrorOccurred(MqttException exception) {
                System.err.println("error: " + exception.getMessage());
            }

            @Override
            public void deliveryComplete(IMqttToken token) { }

            @Override
            public void authPacketArrived(int reasonCode, MqttProperties properties) { }
        });

        client.connect(options);
        client.subscribe(commandFilter, 1);

        MqttMessage online = new MqttMessage("online".getBytes(StandardCharsets.UTF_8));
        online.setQos(1);
        online.setRetained(true);
        client.publish(prefix + "/status", online);

        while (true) {
            String json = String.format("{\"device\":\"%s\",\"temperature\":%.2f,\"ts\":\"%s\"}",
                    clientId, 20 + Math.random() * 5, Instant.now());
            MqttMessage msg = new MqttMessage(json.getBytes(StandardCharsets.UTF_8));
            msg.setQos(1);
            try {
                client.publish(prefix + "/telemetry", msg);
            } catch (MqttException e) {
                System.err.println("publish failed: " + e.getMessage());
            }
            Thread.sleep(5000);
        }
    }
}

Key points: the ssl:// scheme enables TLS; setAutomaticReconnect(true) handles drops; setCleanStart(false) with a session expiry keeps subscriptions across short outages. Resubscribing in connectComplete covers longer ones, but it runs on a worker thread because calling blocking client methods from a Paho callback can stall the client.

Example 2: HiveMQ MQTT Client

import java.nio.charset.StandardCharsets;
import java.time.Instant;

import com.hivemq.client.mqtt.MqttClient;
import com.hivemq.client.mqtt.datatypes.MqttQos;
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;

public class HiveMqDemo {

    static String env(String key, String def) {
        String v = System.getenv(key);
        return (v == null || v.isEmpty()) ? def : v;
    }

    public static void main(String[] args) throws Exception {
        String host = env("MQTT_HOST", "broker.connectmq.com");
        String clientId = env("MQTT_CLIENT_ID", "hivemq-j6t1ve");
        String prefix = env("MQTT_PREFIX", "connectmq/demo/j6t1ve");
        String user = System.getenv("MQTT_USERNAME");

        Mqtt5AsyncClient client = MqttClient.builder()
                .useMqttVersion5()
                .identifier(clientId)
                .serverHost(host)
                .serverPort(8883)
                .sslWithDefaultConfig()
                .automaticReconnectWithDefaultConfig()
                .addConnectedListener(ctx -> System.out.println("connected"))
                .addDisconnectedListener(ctx ->
                        System.out.println("disconnected: " + ctx.getCause().getMessage()))
                .buildAsync();

        var connect = client.connectWith()
                .cleanStart(false)
                .sessionExpiryInterval(600)
                .keepAlive(30)
                .willPublish()
                    .topic(prefix + "/status")
                    .payload("offline".getBytes(StandardCharsets.UTF_8))
                    .qos(MqttQos.AT_LEAST_ONCE)
                    .retain(true)
                    .applyWillPublish();
        if (user != null) {
            connect = connect.simpleAuth()
                    .username(user)
                    .password(env("MQTT_PASSWORD", "").getBytes(StandardCharsets.UTF_8))
                    .applySimpleAuth();
        }
        connect.send().join();

        client.subscribeWith()
                .topicFilter(prefix + "/cmd/#")
                .qos(MqttQos.AT_LEAST_ONCE)
                .callback(publish -> System.out.println("command on " + publish.getTopic() + ": "
                        + new String(publish.getPayloadAsBytes(), StandardCharsets.UTF_8)))
                .send()
                .join();

        client.publishWith()
                .topic(prefix + "/status")
                .payload("online".getBytes(StandardCharsets.UTF_8))
                .qos(MqttQos.AT_LEAST_ONCE)
                .retain(true)
                .send();

        while (true) {
            String json = String.format("{\"device\":\"%s\",\"temperature\":%.2f,\"ts\":\"%s\"}",
                    clientId, 20 + Math.random() * 5, Instant.now());
            client.publishWith()
                    .topic(prefix + "/telemetry")
                    .payload(json.getBytes(StandardCharsets.UTF_8))
                    .qos(MqttQos.AT_LEAST_ONCE)
                    .contentType("application/json")
                    .messageExpiryInterval(60)
                    .send()
                    .whenComplete((result, error) -> {
                        if (error != null) System.err.println("publish failed: " + error.getMessage());
                    });
            Thread.sleep(5000);
        }
    }
}

The builder reads almost like a sentence. sslWithDefaultConfig() enables TLS with the JDK trust store, and automaticReconnectWithDefaultConfig() reconnects with exponential back-off. The MQTT 5 properties contentType and messageExpiryInterval tell consumers the payload format and let the broker drop stale readings after a minute. The MQTT 5 features overview explains these properties in more depth.

Blocking, Async or Reactive?

Both examples use a simple while loop with Thread.sleep so the flow is easy to follow, but real services rarely look like that. The API style you choose shapes the rest of the code, so it is worth deciding early.

Blocking calls are the easiest to read: connect, subscribe and publish each return only when the broker has answered. Paho's MqttClient and the HiveMQ Mqtt5BlockingClient work this way. They suit command-line tools, tests and small gateways where one thread per connection is acceptable.

Asynchronous calls return immediately with a token (IMqttToken in Paho's MqttAsyncClient) or a CompletableFuture (HiveMQ). They suit backend services that publish at high rates, because a slow acknowledgement never stalls the producer thread. Chain error handling onto the future instead of ignoring it, as the HiveMQ example does with whenComplete.

Reactive streams, available only in the HiveMQ client through its RxJava API, model incoming messages as a Flowable with real back-pressure. If your application already uses Reactor or RxJava, this avoids buffering messages in memory when consumers fall behind.

Whatever you choose, keep message handling off the network thread. A database insert or HTTP call inside messageArrived or a HiveMQ callback delays every other message on that connection. Push the payload into a queue or executor and return quickly.

Topic Design for Java Services

The examples use three topic families: .../telemetry for readings, .../cmd/# for commands and .../status for availability. Keeping commands and data apart means a consumer can subscribe to telemetry with a single wildcard without ever seeing commands, and permissions can later be reasoned about by topic family. Put identifiers that you will filter on, such as site, line and device, into topic levels rather than only into the JSON payload, because the broker can only route on topics.

Testing Both Clients

Run either class and watch the topics from a terminal:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-j6t1ve -t 'connectmq/demo/j6t1ve/#' -v
mosquitto_pub -h broker.connectmq.com -p 1883 -i send-j6t1ve -t 'connectmq/demo/j6t1ve/cmd/valve' -m 'close'

Run only one client at a time with the default IDs, or give each its own ID; two clients cannot share a session.

Switching to a Private Broker

For production, create a broker in the ConnectMQ portal and use the generated username, password and client ID. The client ID is bound to the issued credentials, so it must match exactly:

export MQTT_CLIENT_ID=YOUR_CLIENT_ID
export MQTT_USERNAME=YOUR_USERNAME
export MQTT_PASSWORD=YOUR_PASSWORD
export MQTT_PREFIX=warehouse/zone3/gateway

Paho reads the full URI from MQTT_URL (ssl://broker.connectmq.com:8883); the HiveMQ example reads MQTT_HOST.

Java MQTT Client Checklist

  1. Unique client ID per JVM. Multiple instances with one ID knock each other offline.
  2. QoS 1 by default. The Free plan supports QoS 0 and 1; QoS 2 needs a paid plan. See MQTT QoS levels explained.
  3. Do not block callbacks. Hand work to an executor in both libraries.
  4. Handle publish failures. Decide whether to drop, buffer or retry while disconnected.
  5. Log reason codes. MQTT 5 disconnects carry a reason, for example 0x97 when a quota is exceeded.
  6. Keep credentials outside the code, in environment variables or a secret store.

Troubleshooting

Symptom Cause Fix
PKIX path building failed Very old JDK or custom trust store without ISRG Root X1 Update the JDK or import the root
Connects, then disconnects repeatedly Duplicate client ID Use unique IDs
Not authorized Credential or client ID mismatch Re-copy from the portal
Messages stop after a long outage Session expired, no resubscribe Resubscribe after reconnect
ClassNotFoundException for Netty Shaded or trimmed build Include HiveMQ client's transitive dependencies

The official Paho project page is at eclipse.dev/paho.

More Backend Tutorials

If your team also writes Go or Rust, compare with the Go MQTT client tutorial. For a wider survey across languages, read the MQTT client libraries guide.

Connect Your Java Services to ConnectMQ

A hosted MQTT broker lets your Java services connect with one TLS endpoint and no broker to operate. ConnectMQ supports MQTT 3.1.1 and 5.0, so both Paho and the HiveMQ client work unchanged, and the portal's live topic explorer shows exactly what your code publishes. If you are still comparing hosting options, the cloud MQTT broker guide covers the trade-offs. Create a free Serverless broker at app.connectmq.com, no credit card required.

Frequently Asked Questions

Which Java MQTT client library is best?

Both are solid. Paho is dependency-free and widely documented; the HiveMQ MQTT Client has a modern fluent API, async and reactive support, and more active development.

Does Eclipse Paho support MQTT 5 in Java?

Yes, through the separate org.eclipse.paho.mqttv5.client artifact. The older org.eclipse.paho.client.mqttv3 artifact supports MQTT 3.1.1 only.

How do I enable TLS in a Java MQTT client?

With Paho, use an ssl://host:8883 URI. With the HiveMQ client, call sslWithDefaultConfig() and use port 8883. Both rely on the JDK trust store for publicly trusted certificates.

Do Java MQTT clients reconnect automatically?

Yes, if enabled: setAutomaticReconnect(true) in Paho and automaticReconnectWithDefaultConfig() in the HiveMQ client. Resubscribe after reconnects when the session may have expired.

Related articles

Best MQTT CLI Tools for Testing, Scripting and Debugging

A hands-on comparison of mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, the MQTT.js CLI, curl and mqttui, with the same test run against broker.connectmq.com so you can choose the right tool for scripting and debugging.

8 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.