Tutorials

MQTT in the Browser with Plain JavaScript (No Build Step)

Load MQTT.js 5 from a CDN in a single HTML file and connect over secure WebSockets: publish, subscribe, reconnect and debug, with no npm or bundler needed.

Updated · 8 min read

MQTT in the Browser with Plain JavaScript (No Build Step)

You do not need npm, React or a bundler to talk MQTT from a web page. MQTT.js 5 ships a ready-made browser bundle: add one <script> tag, call mqtt.connect('wss://…'), and the page publishes and subscribes over secure WebSockets. This guide builds a complete single-file client that you can open from disk, drop on any static host or paste into an internal tool.

Why browsers need WebSockets

Browsers cannot open raw TCP sockets, so mqtt://host:1883 and mqtts://host:8883 are out of reach for a web page. Brokers solve this with a WebSocket listener: the same MQTT packets travel inside WebSocket frames. For broker.connectmq.com the endpoints are:

URLUse
ws://broker.connectmq.com:8083/mqttPlain WebSocket, only for pages served over http:// or file://
wss://broker.connectmq.com:8084/mqttSecure WebSocket (TLS), works from every page, use this by default

The /mqtt path matters: connecting to the right port with the wrong path typically fails during the HTTP upgrade (see WebSocket path 404). MQTT over WebSockets explains the transport in depth.

The complete page

Save this as index.html. It pins an exact MQTT.js version from the CDN, which exposes a global mqtt object.

index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>MQTT browser client</title>
  <script src="https://unpkg.com/mqtt@5.10.1/dist/mqtt.min.js"></script>
  <style>
    body { font: 15px/1.5 system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
    #log { background: #111; color: #9f9; padding: .75rem; height: 260px; overflow: auto; font-family: monospace; }
    input, button { font: inherit; padding: .4rem .6rem; }
  </style>
</head>
<body>
  <h1>MQTT over WSS</h1>
  <p>Status: <strong id="status">connecting…</strong></p>
  <form id="pub">
    <input id="msg" placeholder="Message" required>
    <button>Publish</button>
  </form>
  <pre id="log"></pre>

  <script>
    const URL = 'wss://broker.connectmq.com:8084/mqtt';
    const TOPIC = 'testmqtt/browser-demo/' + Math.random().toString(16).slice(2, 8);

    const $ = (id) => document.getElementById(id);
    const log = (line) => {
      $('log').textContent += new Date().toLocaleTimeString() + '  ' + line + '\n';
      $('log').scrollTop = $('log').scrollHeight;
    };

    const client = mqtt.connect(URL, {
      clientId: 'web-' + Math.random().toString(16).slice(2, 10),
      // username: 'username', password: 'password',   // see "Credentials" below
      protocolVersion: 5,
      clean: true,
      keepalive: 30,
      connectTimeout: 10_000,
      reconnectPeriod: 2_000,
    });

    client.on('connect', () => {
      $('status').textContent = 'connected';
      client.subscribe(TOPIC, { qos: 1 }, (err) => {
        log(err ? 'subscribe failed: ' + err.message : 'subscribed to ' + TOPIC);
      });
    });

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

    client.on('reconnect', () => { $('status').textContent = 'reconnecting…'; });
    client.on('offline',   () => { $('status').textContent = 'offline'; });
    client.on('error',     (err) => log('error: ' + err.message));

    $('pub').addEventListener('submit', (e) => {
      e.preventDefault();
      client.publish(TOPIC, $('msg').value, { qos: 1 });
      $('msg').value = '';
    });

    window.addEventListener('pagehide', () => client.end());
  </script>
</body>
</html>

Open the file, type a message and press Publish: the page subscribes to its own random topic, so the message comes back through the broker and appears in the log. Each tab gets its own topic and client ID, so two open tabs do not interfere. To see the traffic from outside, open the online MQTT client, subscribe to testmqtt/browser-demo/# and publish from either side.

ES module variant

If you prefer modules, MQTT.js also publishes an ESM build. No global is created, so you import it:

Module script
<script type="module">
  import mqtt from 'https://unpkg.com/mqtt@5.10.1/dist/mqtt.esm.js';

  const client = mqtt.connect('wss://broker.connectmq.com:8084/mqtt', { protocolVersion: 5 });
  client.on('connect', () => client.publish('testmqtt/browser-demo/esm', 'hello from a module'));
</script>

Module scripts are deferred automatically and do not leak variables into window. Some browsers block module scripts on pages opened from file://, so serve the folder with a local web server:

Serve the folder locally
python3 -m http.server 8000
# then open http://localhost:8000/

Options that matter in a browser

OptionRecommendedWhy
clientIdRandom per tabTwo tabs with the same ID kick each other off in a loop
protocolVersion5 (or 4 for 3.1.1)MQTT 5 gives reason codes in errors, which makes debugging easier
keepalive30–60 sLaptops sleep and Wi-Fi changes; a short keep-alive detects dead sockets sooner
reconnectPeriod2,000–5,000 ms0 disables auto-reconnect; very small values hammer the broker
connectTimeout10,000 msFail fast and show the user a state instead of spinning forever
cleantrueA browser tab rarely needs a persistent session

Generate IDs with a prefix you can recognise in broker logs, or use the client ID generator. If you need the same setup in another language, the MQTT code generator produces equivalent snippets.

Credentials in a static page

Everything in an HTML file is public: anyone can press View Source and copy username and password. Treat browser credentials accordingly:

  • Create a dedicated broker user for the page and restrict it with ACLs to exactly the topics it needs. A dashboard usually only needs read access to .../telemetry/#.
  • Never reuse a device, backend or admin account in the browser.
  • For logged-in users, let your backend issue per-user or short-lived credentials after authentication and pass them to mqtt.connect.

MQTT WebSocket authentication in the browser walks through these patterns in detail.

Debugging the connection

Open DevTools (F12) and check two places:

  1. Console. Errors from the error event and the browser's own WebSocket messages show up here.
  2. Network → WS. Select the connection to see the HTTP upgrade (status 101 Switching Protocols on success) and the binary MQTT frames in the Messages tab.
Console messageLikely causeFix
Mixed Content: The page at 'https://…' was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint 'ws://…'ws:// used from an HTTPS pageUse wss://…:8084/mqtt
WebSocket connection to 'wss://…' failedWrong port or path, firewall, TLS problemCheck port 8084 and the /mqtt path; see WebSocket connection failed
connack timeoutWebSocket opened but no CONNACK in timeBroker overloaded or a proxy is buffering; raise connectTimeout and retry
Connection refused: Not authorized / Bad User Name or PasswordMissing or wrong credentialsCheck username and password and the user's ACLs
Status flickers between connected and reconnectingDuplicate client ID in another tab or deviceMake clientId unique per tab

The reason code lookup translates the numeric codes that MQTT 5 brokers return. To rule out your page entirely, try the same URL in the online client: if it connects there, the problem is in your code, not the broker or network.

Watch the demo page's topic in the online client

Where to go from here

The page above is a complete client. Typical next steps are a chat UI (one topic per room, presence via Last Will), a live chart fed by sensor telemetry, or a control panel that publishes commands. When the code grows past a few hundred lines, moving to the npm package and a bundler is easy: the mqtt.connect API is identical, which is also why the MQTT.js guide applies unchanged.

One script tag, one wss:// URL and a handful of event handlers are all a browser needs to speak MQTT. Pin the library version, give every tab its own client ID, ship only a restricted account to the browser, and verify each change in DevTools and the online client before you deploy.

Frequently asked questions

Can a browser connect to MQTT on port 1883 or 8883?

No. Browsers cannot open raw TCP sockets, so a web page can only reach an MQTT broker through a WebSocket listener. Use wss:// (port 8084 with path /mqtt on broker.connectmq.com) from any page served over HTTPS.

Is it safe to put MQTT credentials in a static HTML page?

Anything in the page is readable by every visitor. Use a dedicated broker user whose ACL only allows the topics that page needs (often read-only), or have your backend hand out short-lived credentials. Never ship an admin or device account to the browser.

Why does my page work locally but fail when deployed?

Usually mixed content. A page served over https:// may not open an insecure ws:// connection, so the browser blocks it. Switch the URL to wss:// on the TLS WebSocket port and check the path.