Tutorials

MQTT in JavaScript: MQTT.js Tutorial for Node.js and the Browser

Learn MQTT in JavaScript with MQTT.js v5: connect, subscribe and publish in Node.js and in the browser over WebSockets, async/await, React and error handling.

Updated · 8 min read

MQTT.js is the most widely used MQTT client for JavaScript. The same package runs in Node.js (over TCP, TLS or WebSockets) and in the browser (over WebSockets), supports MQTT 3.1.1 and 5, and since version 5 ships TypeScript types and promise-based methods. This tutorial covers the v5 API end to end.

Install MQTT.js

bash
npm install mqtt

The examples use ES modules (import) and the public HiveMQ sandbox. Save them as .mjs files or set "type": "module" in package.json.

Connection URLs: which protocol where

URL schemeTransportTypical portNode.jsBrowser
mqtt://Plain TCP1883YesNo
mqtts://TLS8883YesNo
ws://WebSocket8000, 8083, 80YesOnly on http:// pages
wss://Secure WebSocket8884, 8084, 443YesYes

Ports and WebSocket paths differ per broker — check the HiveMQ or EMQX pages, or read MQTT ports explained.

Subscribe and publish in Node.js

The classic event-based API: mqtt.connect() returns a client immediately and emits events as the connection progresses.

client.mjs
import mqtt from 'mqtt';

const topic = 'testmqtt/js-demo/hello';

const client = mqtt.connect('mqtt://broker.hivemq.com:1883', {
  clientId: `js-demo-${Math.random().toString(16).slice(2, 10)}`,
  clean: true,
  keepalive: 60,
  reconnectPeriod: 2000, // ms between reconnect attempts; 0 disables
  connectTimeout: 10_000,
});

client.on('connect', () => {
  console.log('connected');
  client.subscribe('testmqtt/js-demo/#', { qos: 1 }, (err) => {
    if (err) return console.error('subscribe failed:', err.message);
    client.publish(topic, JSON.stringify({ hello: 'world' }), { qos: 1 });
  });
});

client.on('message', (topic, payload, packet) => {
  console.log(`${topic} (retain=${packet.retain}): ${payload.toString()}`);
});

client.on('reconnect', () => console.log('reconnecting...'));
client.on('offline', () => console.log('offline'));
client.on('error', (err) => console.error('error:', err.message));

payload is a Buffer — call toString() for text or JSON.parse(payload.toString()) for JSON. The connect event fires again after every reconnect; with clean: true the broker forgets your subscriptions, which is why subscribing inside the handler is the safe pattern. MQTT.js also resubscribes on reconnect by default (resubscribe: true).

Async/await with connectAsync

For scripts, tests and serverless functions, the promise API in MQTT.js v5 reads much more naturally:

async.mjs
import mqtt from 'mqtt';

const client = await mqtt.connectAsync('mqtts://broker.hivemq.com:8883', {
  protocolVersion: 5,
});

client.on('message', (topic, payload) => {
  console.log(topic, payload.toString());
});

try {
  const granted = await client.subscribeAsync('testmqtt/js-demo/#', { qos: 1 });
  console.log('granted:', granted.map((g) => `${g.topic} QoS ${g.qos}`).join(', '));

  await client.publishAsync('testmqtt/js-demo/async', 'sent with publishAsync', {
    qos: 1,
    retain: false,
  });

  await new Promise((resolve) => setTimeout(resolve, 2000));
} finally {
  await client.endAsync();
}

connectAsync() rejects when the client emits an error before connecting — for example when the broker rejects your credentials — so you can handle it with a normal try/catch. For an unreachable host it keeps retrying by default; pass false as the third argument to fail fast instead. publishAsync() resolves once the broker acknowledges a QoS 1 or 2 message. See MQTT QoS levels for what each level guarantees.

Watch your MQTT.js messages live

TLS, credentials and MQTT 5 options in Node.js

Production brokers require TLS and authentication. Credentials go into the connect options; for a broker with a publicly trusted certificate, mqtts:// is all you need. For a private CA, pass the CA certificate (and, for mutual TLS, a client certificate and key):

secure.mjs
import { readFileSync } from 'node:fs';
import mqtt from 'mqtt';

const client = await mqtt.connectAsync('mqtts://broker.example.com:8883', {
  clientId: 'billing-service-01',
  username: process.env.MQTT_USERNAME,
  password: process.env.MQTT_PASSWORD,
  protocolVersion: 5,
  clean: false,
  properties: { sessionExpiryInterval: 3600 },
  // Only for a private CA / mutual TLS:
  // ca: readFileSync('ca.crt'),
  // cert: readFileSync('client.crt'),
  // key: readFileSync('client.key'),
  will: {
    topic: 'services/billing/status',
    payload: Buffer.from('offline'),
    qos: 1,
    retain: true,
  },
});

await client.publishAsync('services/billing/status', 'online', { qos: 1, retain: true });
await client.publishAsync('orders/created', JSON.stringify({ id: 42 }), {
  qos: 1,
  properties: {
    contentType: 'application/json',
    messageExpiryInterval: 60,
    userProperties: { source: 'billing' },
  },
});

Never set rejectUnauthorized: false outside local testing — it turns off certificate verification. Keep credentials in environment variables rather than source code. The will option registers a Last Will that the broker publishes if the service dies without disconnecting, and retain: true keeps the latest status available to new subscribers (see retained messages). Whether you need the MQTT 5 properties at all is covered in MQTT 5 vs 3.1.1.

Shutting down cleanly

A long-running Node.js service should disconnect gracefully so in-flight QoS 1/2 messages finish and the broker does not fire your Last Will on a normal restart:

javascript
process.on('SIGTERM', async () => {
  await client.publishAsync('services/billing/status', 'offline', { qos: 1, retain: true });
  await client.endAsync();
  process.exit(0);
});

MQTT.js in the browser

In the browser, only WebSocket URLs work. On an HTTPS page you must use wss://, otherwise the browser blocks the connection as mixed content:

browser.js
import mqtt from 'mqtt';

const client = mqtt.connect('wss://broker.hivemq.com:8884/mqtt', {
  clientId: `web-${crypto.randomUUID().slice(0, 8)}`,
});

client.on('connect', () => client.subscribe('testmqtt/js-demo/#'));
client.on('message', (topic, payload) => {
  const li = document.createElement('li');
  li.textContent = `${topic}: ${payload.toString()}`;
  document.querySelector('#messages').append(li);
});

Modern bundlers such as Vite, webpack 5 and the Next.js compiler pick up the browser build when you import mqtt from 'mqtt'. If a bundler complains about Node built-ins, import the pre-built bundle from mqtt/dist/mqtt.min.js instead. Without a bundler, load that same file from a CDN with a <script> tag and use the global mqtt object. The MQTT over WebSockets guide covers paths and ports in depth.

Using MQTT.js in React

The most common bug is creating a new client on every render. Create the client once inside useEffect and always end it in the cleanup — React StrictMode mounts effects twice in development, and without cleanup you get duplicate connections and duplicate messages:

useMqtt.ts
import { useEffect, useState } from 'react';
import mqtt from 'mqtt';

export function useMqttMessages(url: string, topic: string) {
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    const client = mqtt.connect(url);
    client.on('connect', () => client.subscribe(topic));
    client.on('message', (_t, payload) => {
      setMessages((prev) => [...prev.slice(-99), payload.toString()]);
    });
    return () => {
      client.end();
    };
  }, [url, topic]);

  return messages;
}

In Next.js, MQTT.js uses browser APIs, so call it only from Client Components ('use client') or from server-side route handlers using mqtt:// or mqtts://. For larger apps, put a single client in a context provider and share it.

Error handling

  • Always attach an error listener. In Node.js an unhandled error event crashes the process.
  • Bad credentials keep retrying. With the event API, a rejected CONNECT emits error and MQTT.js keeps reconnecting. Inspect err.code and call client.end() if retrying is pointless. Look up codes with the MQTT reason code tool.
  • Endless reconnect loops with an immediate disconnect usually mean a duplicate client ID or an ACL rejecting your subscription. See the MQTT connection errors guide.
  • Offline buffering. While disconnected, MQTT.js queues publishes in memory and sends them on reconnect. Set queueQoSZero: false if stale QoS 0 data should be dropped instead.

Need a starting point for your own broker? The MQTT code generator generates this code for your host, port, credentials and topic — then test it live in the online MQTT client.

Frequently asked questions

Can MQTT.js connect to port 1883 from the browser?

No. Browsers cannot open raw TCP sockets, so MQTT.js in the browser must use a ws:// or wss:// URL pointing at the broker’s WebSocket listener, for example wss://broker.hivemq.com:8884/mqtt. In Node.js you can use mqtt://, mqtts://, ws:// or wss://.

How do I use async/await with MQTT.js?

MQTT.js v5 provides promise-based methods: mqtt.connectAsync() to connect, and client.subscribeAsync(), client.publishAsync() and client.endAsync() on the returned client. Incoming messages are still delivered through the message event.

Why does my React app open two MQTT connections?

React StrictMode mounts components twice in development, so an effect that connects without a cleanup creates two clients. Create the client inside useEffect and call client.end() in the cleanup function, or keep a single client in a module or context.