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:
| URL | Use |
|---|---|
ws://broker.connectmq.com:8083/mqtt | Plain WebSocket, only for pages served over http:// or file:// |
wss://broker.connectmq.com:8084/mqtt | Secure 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.
<!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:
<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:
python3 -m http.server 8000
# then open http://localhost:8000/Options that matter in a browser
| Option | Recommended | Why |
|---|---|---|
clientId | Random per tab | Two tabs with the same ID kick each other off in a loop |
protocolVersion | 5 (or 4 for 3.1.1) | MQTT 5 gives reason codes in errors, which makes debugging easier |
keepalive | 30–60 s | Laptops sleep and Wi-Fi changes; a short keep-alive detects dead sockets sooner |
reconnectPeriod | 2,000–5,000 ms | 0 disables auto-reconnect; very small values hammer the broker |
connectTimeout | 10,000 ms | Fail fast and show the user a state instead of spinning forever |
clean | true | A 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:
- Console. Errors from the
errorevent and the browser's own WebSocket messages show up here. - Network → WS. Select the connection to see the HTTP upgrade (status
101 Switching Protocolson success) and the binary MQTT frames in the Messages tab.
| Console message | Likely cause | Fix |
|---|---|---|
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 page | Use wss://…:8084/mqtt |
WebSocket connection to 'wss://…' failed | Wrong port or path, firewall, TLS problem | Check port 8084 and the /mqtt path; see WebSocket connection failed |
connack timeout | WebSocket opened but no CONNACK in time | Broker overloaded or a proxy is buffering; raise connectTimeout and retry |
Connection refused: Not authorized / Bad User Name or Password | Missing or wrong credentials | Check username and password and the user's ACLs |
| Status flickers between connected and reconnecting | Duplicate client ID in another tab or device | Make 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.


