IoT Tutorials

MQTT.js Node.js Tutorial: Publish, Subscribe, QoS and TLS

Build a Node.js publisher and subscriber with MQTT.js v5, use the async API, QoS and Last Will, encrypt with TLS on port 8883, and tune reconnect options so your service survives network drops.

Node.js terminal output from an mqtt.js node.js publisher and subscriber connected to a cloud broker 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

MQTT.js is the standard MQTT client for JavaScript, and using MQTT.js in Node.js is the fastest way to build IoT backends, bridges and test tools. In this tutorial you will create a Node.js publisher and subscriber with MQTT.js v5, use its promise-based API, choose QoS levels, add a Last Will, switch to TLS and configure reconnects for a long-running service. All examples run against the free public broker broker.connectmq.com, so you only need Node.js installed.

Prerequisites

  • Node.js 18 or newer (node --version)
  • npm, pnpm or yarn
  • Optional: mosquitto_sub to watch traffic from a second terminal

Create a project and install the library:

mkdir mqtt-node-demo && cd mqtt-node-demo
npm init -y
npm pkg set type=module
npm install mqtt

Setting "type": "module" lets you use import syntax and top-level await. MQTT.js v5 is written in TypeScript and ships its own type definitions, so editors give you autocompletion without extra packages.

Connection URLs and Ports

MQTT.js picks the transport from the URL scheme. On ConnectMQ brokers the ports are:

Scheme Port Transport Typical use in Node.js
mqtt:// 1883 Plain TCP Local testing only
mqtts:// 8883 TCP with TLS Production services
ws:// 8083 WebSocket Rarely needed in Node.js
wss:// 8084 (path /mqtt) Secure WebSocket Browsers, restrictive proxies

In Node.js, prefer mqtts:// on 8883. WebSocket matters for browsers, which cannot open raw TCP sockets; the MQTT over WebSocket article covers that side.

Your First MQTT.js Subscriber

Save as subscriber.js:

import mqtt from "mqtt";

const PREFIX = "connectmq/demo/node-a71c";
const clientId = `node-sub-${Math.random().toString(16).slice(2, 10)}`;

const client = mqtt.connect("mqtts://broker.connectmq.com:8883", {
  clientId,
  protocolVersion: 5,
  clean: true,
  keepalive: 30,
  reconnectPeriod: 2000,   // wait 2 s between reconnect attempts
  connectTimeout: 10000,   // give up a single attempt after 10 s
});

client.on("connect", async (connack) => {
  console.log(`Connected as ${clientId}, session present: ${connack.sessionPresent}`);
  const granted = await client.subscribeAsync(`${PREFIX}/+/telemetry`, { qos: 1 });
  console.log("Subscribed:", granted.map((g) => `${g.topic} (qos ${g.qos})`).join(", "));
});

client.on("message", (topic, payload, packet) => {
  let data;
  try {
    data = JSON.parse(payload.toString());
  } catch {
    data = payload.toString();
  }
  const device = topic.split("/").at(-2);
  console.log(`[${device}] qos=${packet.qos} retain=${packet.retain}`, data);
});

client.on("reconnect", () => console.log("Reconnecting..."));
client.on("offline", () => console.log("Offline"));
client.on("error", (err) => console.error("MQTT error:", err.message));

process.on("SIGINT", async () => {
  await client.endAsync();
  process.exit(0);
});

Subscribing inside the connect handler is deliberate. With clean: true, the broker forgets subscriptions when the connection drops, so re-subscribing on every connect guarantees you keep receiving messages after a reconnect.

Publishing with QoS and a Last Will

Save as publisher.js:

import mqtt from "mqtt";

const PREFIX = "connectmq/demo/node-a71c";
const DEVICE = "sensor-01";
const clientId = `node-pub-${Math.random().toString(16).slice(2, 10)}`;

const client = await mqtt.connectAsync("mqtts://broker.connectmq.com:8883", {
  clientId,
  protocolVersion: 5,
  will: {
    topic: `${PREFIX}/${DEVICE}/status`,
    payload: Buffer.from("offline"),
    qos: 1,
    retain: true,
  },
});

console.log("Connected");
await client.publishAsync(`${PREFIX}/${DEVICE}/status`, "online", { qos: 1, retain: true });

let count = 0;
const timer = setInterval(async () => {
  const reading = {
    temperature: +(20 + Math.random() * 5).toFixed(2),
    humidity: +(40 + Math.random() * 10).toFixed(1),
    ts: Date.now(),
  };
  try {
    await client.publishAsync(`${PREFIX}/${DEVICE}/telemetry`, JSON.stringify(reading), {
      qos: 1,
      properties: { messageExpiryInterval: 60, contentType: "application/json" },
    });
    console.log("Published", reading);
  } catch (err) {
    console.error("Publish failed:", err.message);
  }
  if (++count === 20) {
    clearInterval(timer);
    await client.publishAsync(`${PREFIX}/${DEVICE}/status`, "offline", { qos: 1, retain: true });
    await client.endAsync();
  }
}, 2000);

Run both:

node subscriber.js
# in a second terminal
node publisher.js

A few things are happening here:

  • connectAsync resolves once the broker sends CONNACK, which is convenient in scripts. In long-running services, the event-based connect is still useful because it fires again after every reconnect.
  • QoS 1 means the broker acknowledges each publish, and publishAsync resolves only after that acknowledgement. For high-rate telemetry where an occasional loss is fine, QoS 0 is cheaper. The trade-offs are covered in MQTT QoS levels explained.
  • The Last Will is stored by the broker at connect time and published if the client disappears without a clean disconnect, so other services learn the sensor is offline. Retaining it means new subscribers see the current status immediately.
  • MQTT 5 properties such as messageExpiryInterval tell the broker to drop the message if it cannot be delivered within 60 seconds, which avoids stale readings piling up for offline subscribers.

The public area of the free public MQTT broker is readable by anyone, so change the node-a71c part of the prefix to your own random string and never send real data there.

Reconnect Options That Matter

MQTT.js reconnects automatically. These options decide how it behaves:

Option Default Recommendation
reconnectPeriod 1000 ms 2000 to 5000 ms for services; 0 disables reconnects
connectTimeout 30000 ms 10000 ms so a dead network is detected faster
keepalive 60 s 30 to 60 s; lower detects silent drops sooner
clean true false with a stable client ID if you need queued messages
properties.sessionExpiryInterval 0 Seconds the broker keeps the session after disconnect (MQTT 5)
queueQoSZero true Set to false if stale QoS 0 messages should not be sent after reconnect

For a service that must not miss messages while briefly offline, combine a fixed client ID, clean: false, QoS 1 subscriptions and a session expiry interval. The broker then queues messages for the session and delivers them when the client returns.

Structuring a Long-Running Node.js Service

The scripts above are deliberately short. A service that runs for months needs a little more structure, and most of it is about lifecycle rather than MQTT itself:

  • Create one client per process and share it through a module. Opening a new connection per request or per job wastes resources and quickly hits connection limits.
  • Treat connect as "ready". Gate work that needs the broker on the client's connected property, and decide what to do when it is false: buffer, drop or return an error to the caller.
  • Handle shutdown signals. On SIGTERM (sent by Docker, Kubernetes and systemd), publish a final status if you use one, call endAsync() so in-flight QoS 1 messages are flushed, and only then exit.
  • Log state transitions, not every message. connect, reconnect, offline, close and error events are what you need when debugging an outage. Per-message logs drown them out.
  • Keep handlers fast. The message event runs on Node's event loop. Heavy work, such as database writes, should be awaited carefully or pushed to a queue so a slow downstream system does not delay every other message.

These habits matter more than any single option value, and they apply equally to bridges, workers and test tools built on MQTT.js.

Authenticating with a Private Broker

For production, use credentials from your broker. With ConnectMQ, you create a broker in the portal and receive a username, password and client ID. Load them from environment variables:

import mqtt from "mqtt";

const client = mqtt.connect(`mqtts://${process.env.MQTT_HOST}:8883`, {
  clientId: process.env.MQTT_CLIENT_ID,   // YOUR_CLIENT_ID from the portal
  username: process.env.MQTT_USERNAME,    // YOUR_USERNAME
  password: process.env.MQTT_PASSWORD,    // YOUR_PASSWORD
  protocolVersion: 5,
  clean: false,
  properties: { sessionExpiryInterval: 3600 },
});

client.on("connect", () => console.log("Connected to private broker"));
client.on("error", (err) => console.error(err.message));

ConnectMQ uses a Let's Encrypt certificate, which Node.js trusts out of the box, so you do not need to pass a CA file. Never set rejectUnauthorized: false in production; it disables certificate checks and makes TLS pointless.

Troubleshooting MQTT.js in Node.js

  1. Connects, then disconnects every few seconds. Another client uses the same client ID. Each running process needs its own ID.
  2. ERR_REQUIRE_ESM or Cannot use import statement. Set "type": "module" in package.json, or use const mqtt = require("mqtt") in CommonJS files.
  3. No messages after a network drop. You subscribed only once outside the connect handler while using clean: true.
  4. connack timeout. A firewall is blocking port 8883. Test with mosquitto_sub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs -i test-1 -t test or try wss:// on 8084.
  5. Subscription granted with QoS 128 or a reason code of 0x80 or higher. The broker rejected the subscription, often because of an invalid filter or a subscription limit on the public broker (10 per client).
  6. Memory grows while offline. Outgoing messages are queued until reconnect. Cap your own buffer or use QoS 0 with queueQoSZero: false for disposable telemetry.

Next Steps

From here you can bring the same client into a frontend: React with MQTT builds a hooks-based dashboard on the browser build of MQTT.js, and type-safe MQTT in TypeScript adds typed topics and validated payloads on top of this code. For other runtimes, the MQTT client libraries guide compares the main options. The full option list lives in the MQTT.js README.

Get Started with ConnectMQ

Move your Node.js services from the public test broker to a private one in minutes. Sign up at app.connectmq.com, choose the free Serverless plan with no credit card, create a broker and drop the generated credentials into your environment. With a managed MQTT broker, ConnectMQ runs the TLS endpoints and broker infrastructure while you focus on your application code.

Frequently Asked Questions

Is MQTT.js v5 compatible with older code?

Mostly. The callback API still works, and v5 adds promise-based methods such as connectAsync, publishAsync, subscribeAsync and endAsync. The main changes are the TypeScript rewrite and a newer minimum Node.js version.

Should I use MQTT 3.1.1 or MQTT 5 with MQTT.js?

Use protocolVersion: 5 when your broker supports it, as ConnectMQ does. You get reason codes, message expiry, session expiry and user properties, while the publish and subscribe code stays the same.

How many connections should a Node.js service open?

Usually one. A single MQTT.js client can publish and subscribe to many topics concurrently, and each extra connection needs its own unique client ID and counts toward your plan's connection limit.

Can I use MQTT.js with Bun or Deno?

MQTT.js targets Node.js and browsers. Runtimes with good Node.js compatibility can often run it, but test TLS and reconnect behaviour carefully before relying on it.

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.