IoT Tutorials

MQTT over WebSocket: Connect a Browser to an MQTT Broker with MQTT.js

Browsers cannot open raw TCP sockets, so web apps speak MQTT over WebSocket. Build a working browser client and a Node.js client against broker.connectmq.com and learn how to fix the most common connection errors.

Browser dashboard connecting to an MQTT broker over a secure WebSocket (MQTT over WebSocket)
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 over WebSocket lets a web page publish and subscribe to an MQTT broker directly from the browser, with no backend relay in between. In this tutorial you will learn why browsers need WebSocket transport, the difference between ws and wss, and how to build a complete HTML page and a Node.js client that talk to broker.connectmq.com.

New to brokers? Read What Is an MQTT Broker? first for the publish/subscribe basics.

Why Browsers Need MQTT over WebSocket

Native MQTT runs over a plain TCP connection, usually on port 1883 (or 8883 with TLS). Desktop apps, servers and microcontrollers can open TCP sockets freely. Browsers cannot: for security reasons, JavaScript in a web page only gets HTTP, WebSocket and a few other browser-managed transports.

WebSocket solves this. It starts as an ordinary HTTP request with an Upgrade: websocket header, and once the server answers 101 Switching Protocols, the connection becomes a full-duplex binary channel. MQTT packets (CONNECT, PUBLISH, SUBSCRIBE and so on) are then carried unchanged inside WebSocket frames. The client asks for the mqtt subprotocol during the handshake so the broker knows what to expect.

Browser (MQTT.js)                                broker.connectmq.com:8084
    |-- TLS handshake ------------------------------>|
    |-- GET /mqtt  Upgrade: websocket                 |
    |   Sec-WebSocket-Protocol: mqtt ---------------->|
    |<-- 101 Switching Protocols ---------------------|
    |== WebSocket frame [ MQTT CONNECT ] ============>|
    |<= WebSocket frame [ MQTT CONNACK ] =============|
    |== WebSocket frame [ MQTT SUBSCRIBE ] ==========>|
    |<= WebSocket frame [ MQTT PUBLISH ] =============|

From the broker's point of view, a WebSocket client is just another MQTT client. It can talk to devices connected over TCP, share topics with them and use the same QoS levels, retained messages and MQTT 5 features.

Typical uses for MQTT over WebSocket:

  • Live IoT dashboards that update the moment a sensor publishes
  • Browser-based control panels that send commands to devices
  • Monitoring tools and digital twins that visualise machine state in real time
  • Hybrid mobile apps built on web technologies

ws vs wss: Ports and Paths on ConnectMQ

Like HTTP and HTTPS, WebSocket has an unencrypted (ws://) and a TLS-encrypted (wss://) form.

Transport Scheme Port Full URL When to use
MQTT (TCP) mqtt:// 1883 mqtt://broker.connectmq.com:1883 Devices, servers, CLI
MQTT over TLS mqtts:// 8883 mqtts://broker.connectmq.com:8883 Devices and servers in production
WebSocket ws:// 8083 ws://broker.connectmq.com:8083/mqtt Local tests from an http:// page
Secure WebSocket wss:// 8084 wss://broker.connectmq.com:8084/mqtt Browsers and production web apps

Two details matter:

  • The path is /mqtt. A WebSocket URL is an HTTP URL, so it has a path. Connecting to wss://broker.connectmq.com:8084 without /mqtt fails the handshake.
  • Prefer wss. Pages served over HTTPS may only open wss:// connections, and credentials sent over plain ws:// travel in clear text. ConnectMQ serves WSS on 8084 with a Let's Encrypt certificate, so browsers trust it without extra setup.

Build an MQTT over WebSocket Page with MQTT.js

The following page is complete and self-contained. Save it as index.html and open it in a browser. It loads MQTT.js from a CDN, connects anonymously to the public area of broker.connectmq.com, subscribes to a unique topic and lets you publish messages.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MQTT over WebSocket demo</title>
  <script src="https://unpkg.com/mqtt@5.10.3/dist/mqtt.min.js"></script>
</head>
<body>
  <p>Status: <span id="status">connecting...</span></p>
  <input id="msg" value="Hello from the browser">
  <button id="send">Publish</button>
  <pre id="log"></pre>

  <script>
    const id = Math.random().toString(16).slice(2, 10);
    const clientId = "web-" + id;
    const topic = "connectmq/demo/" + id + "/chat";
    const log = (line) => {
      document.getElementById("log").textContent += line + "\n";
    };

    const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
      clientId: clientId,
      protocolVersion: 5,
      clean: true,
      keepalive: 30,
      reconnectPeriod: 2000,
      connectTimeout: 10000
    });

    client.on("connect", () => {
      document.getElementById("status").textContent = "connected as " + clientId;
      client.subscribe(topic, { qos: 1 }, (err, granted) => {
        if (err) return log("Subscribe error: " + err.message);
        log("Subscribed to " + granted[0].topic + " (QoS " + granted[0].qos + ")");
      });
    });

    client.on("message", (t, payload) => {
      log("[" + t + "] " + payload.toString());
    });

    client.on("reconnect", () => {
      document.getElementById("status").textContent = "reconnecting...";
    });

    client.on("error", (err) => log("Error: " + err.message));

    document.getElementById("send").onclick = () => {
      const text = document.getElementById("msg").value;
      client.publish(topic, text, { qos: 1 });
    };
  </script>
</body>
</html>

When you click Publish, the message goes to the broker over the secure WebSocket and comes straight back through your own subscription. Open the page in two tabs and you will see that each tab gets its own random topic, so they do not see each other. To share a topic, hard-code the same connectmq/demo/<your-id>/chat topic in both.

A few notes on the options:

  • clientId must be non-empty and unique. The public area rejects an empty client ID, and two clients with the same ID keep kicking each other off.
  • protocolVersion: 5 uses MQTT 5. Use 4 for MQTT 3.1.1 if you need it.
  • keepalive: 30 makes the client send a PINGREQ when the connection is idle, so the broker and any proxies know it is still alive.
  • reconnectPeriod: 2000 makes MQTT.js reconnect automatically two seconds after a drop.

You can also watch the browser's messages from a terminal with the Mosquitto CLI, because TCP and WebSocket clients share the same topics. Replace abc12345 with the id shown in the page's topic:

mosquitto_sub -h broker.connectmq.com -p 1883 -i cli-watch-abc12345 -t "connectmq/demo/abc12345/#" -v

MQTT over WebSocket in Node.js

Node.js can use plain TCP, but WebSocket is useful when your server sits behind a proxy or firewall that only allows HTTPS-style traffic, or when you want to share code with a browser app. Install MQTT.js with npm install mqtt and save this as ws-client.mjs:

import mqtt from "mqtt";

const id = Math.random().toString(16).slice(2, 10);
const clientId = `node-${id}`;
const topic = `connectmq/demo/${id}/hello`;

const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
  clientId,
  protocolVersion: 5,
  keepalive: 30,
});

client.on("connect", (connack) => {
  console.log("Connected, CONNACK properties:", connack.properties);
  client.subscribe(topic, { qos: 1 }, (err, granted) => {
    if (err) throw err;
    console.log("Granted:", granted);
    client.publish(topic, JSON.stringify({ temp: 21.5, ts: Date.now() }), { qos: 1 });
  });
});

client.on("message", (t, payload) => {
  console.log(`Received on ${t}: ${payload.toString()}`);
  client.end();
});

client.on("error", (err) => {
  console.error("Connection error:", err.message);
  client.end();
});

Run it with node ws-client.mjs. The output looks similar to this:

Connected, CONNACK properties: { receiveMaximum: 16, topicAliasMaximum: 65535 }
Granted: [ { topic: 'connectmq/demo/4df8521c/hello', qos: 1, nl: false, rap: false, rh: 0 } ]
Received on connectmq/demo/4df8521c/hello: {"temp":21.5,"ts":1790000000000}

The only difference from a TCP client is the URL. Swap it for mqtt://broker.connectmq.com:1883 and the rest of the code stays the same. If you want to understand the qos values above, see MQTT QoS Levels Explained.

Troubleshooting MQTT over WebSocket Connections

Most problems show up as a connection that never reaches connect or keeps reconnecting. Open the browser developer tools, check the Console and the Network tab (filter by WS), and compare with this table.

Symptom Likely cause Fix
"Mixed Content" error, connection blocked An https:// page is trying to open ws:// Use wss://broker.connectmq.com:8084/mqtt
Handshake fails, HTTP 404 in the Network tab Missing or wrong path Add /mqtt to the URL
Handshake fails on 8083 or 8084 Scheme and port mixed up (ws on 8084, wss on 8083) ws goes with 8083, wss with 8084
You suspect a CORS problem WebSocket is not governed by CORS like fetch/XHR Look for the real cause: URL, port, TLS or credentials
Connects, then disconnects every few seconds Two clients using the same client ID Generate a unique ID per tab or device
Connection refused right after CONNECT Empty client ID, or wrong username/password Set a non-empty client ID; check portal credentials
Drops after a period of inactivity Keepalive too long for a proxy or mobile network Use keepalive: 30 or lower
Works locally, fails on a corporate network Proxy or firewall blocks non-standard ports Test from another network, or ask IT to allow outbound traffic to port 8084

Two of these deserve a closer look.

Mixed content

Browsers block insecure subresources on secure pages. If your dashboard is served over https://, any ws:// connection is refused before it leaves the browser, and the broker never sees it. The fix is always wss://. The unencrypted ws://broker.connectmq.com:8083/mqtt endpoint is only useful for pages opened from http://localhost or a local file.

Client ID conflicts

MQTT allows only one connection per client ID. When a second connection arrives with the same ID, the broker closes the first one. In a browser this often happens when a user opens the same page in two tabs and the client ID is hard-coded. With automatic reconnect enabled, both tabs then fight over the ID forever. Append a random suffix per tab. On a private ConnectMQ broker, client IDs are restricted to the issued client ID and its _suffix variants, so use YOUR_CLIENT_ID_tab1, YOUR_CLIENT_ID_tab2 and so on.

Production: Using Credentials from the Portal

The public area is for testing: anything published there is readable by anyone, and it has tight limits (64 KB per message, 10 messages per second per client). For a real application, create a private broker in the portal and connect with the generated credentials:

const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
  clientId: "YOUR_CLIENT_ID_dashboard",
  username: "YOUR_USERNAME",
  password: "YOUR_PASSWORD",
  protocolVersion: 5,
  keepalive: 30,
});

Keep these points in mind for browser apps:

  • Always use wss in production. The username and password are sent in the CONNECT packet, so the transport must be encrypted.
  • Treat browser credentials as visible. Anything shipped to a browser can be read by its user. Give the web app its own credentials rather than reusing device credentials, and avoid embedding them in public pages.
  • Respect plan limits. Each open tab is a connection. The Serverless (Free) plan allows up to 25 concurrent connections, which suits prototypes. Production dashboards fit better on Serverless (Paid) with up to 1,000 connections, or a Dedicated cluster.
  • Handle reconnects gracefully. Show a status indicator, and resubscribe or rely on a persistent session when the connection returns.

More hardening advice is collected in MQTT Security Best Practices.

Get Started with ConnectMQ

You can test MQTT over WebSocket right now with the public endpoint wss://broker.connectmq.com:8084/mqtt. When you are ready to build a real dashboard, sign up for free at app.connectmq.com (free Serverless plan, no credit card), create a broker and connect with your own username, password and client ID. The portal's built-in browser MQTT client and live topic explorer make it easy to check that your web app is publishing what you expect.

Frequently Asked Questions

What port does MQTT over WebSocket use?

There is no single standard port. On broker.connectmq.com, unencrypted WebSocket uses port 8083 and secure WebSocket uses port 8084, both with the path /mqtt.

Is MQTT over WebSocket slower than MQTT over TCP?

The overhead is small. After the one-time HTTP upgrade, each MQTT packet is wrapped in a WebSocket frame that adds only a few bytes, so latency is close to plain TCP for most applications.

Do I need a backend server to use MQTT in the browser?

No. With MQTT over WebSocket the browser connects directly to the broker. A backend is only needed if you want to keep credentials off the client or add your own business logic.

Does CORS apply to MQTT over WebSocket?

No, WebSocket connections are not subject to CORS preflight checks the way fetch and XHR requests are. If a connection fails, check the URL, port, path and credentials instead.

Can browser clients and ESP32 devices share the same topics?

Yes. The broker routes messages by topic regardless of transport, so a browser on WSS and a device on TCP port 1883 can publish and subscribe to the same topics. See the ESP32 MQTT tutorial for the device side.

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.