Running MQTT in the browser with JavaScript is the fastest way to build a live dashboard, a remote control panel or a debugging tool without writing a backend. In this tutorial you will create one self-contained HTML page that connects to broker.connectmq.com over secure WebSocket, subscribes to a topic, publishes messages and renders them in real time. You will also learn why browsers need WebSocket for MQTT, why CORS is not a problem, and how to move the same page to a private broker safely.
Why a Browser Needs MQTT over WebSocket
A web page cannot open raw TCP sockets. The browser sandbox only lets JavaScript talk to the network through HTTP(S), WebSocket, WebRTC and a few newer APIs. Standard MQTT runs over TCP on port 1883 (or TLS on 8883), so a browser cannot reach those ports directly.
The MQTT specification solves this by allowing MQTT packets to be carried inside WebSocket binary frames. The broker exposes an extra listener, the browser opens a WebSocket to it, and from then on the exchange is ordinary MQTT: CONNECT, SUBSCRIBE, PUBLISH, PINGREQ. If you want the protocol-level detail, our guide to MQTT over WebSocket walks through the handshake and the subprotocol header.
ConnectMQ exposes two WebSocket listeners on every broker:
| Endpoint | URL | When to use |
|---|---|---|
| WebSocket | ws://broker.connectmq.com:8083/mqtt |
Local testing from an http:// page only |
| Secure WebSocket | wss://broker.connectmq.com:8084/mqtt |
Any page served over https://, and all production use |
Use wss:// by default. A page loaded over HTTPS is not allowed to open an unencrypted ws:// connection; the browser blocks it as mixed content before any packet leaves the machine.
What about CORS?
CORS (Cross-Origin Resource Sharing) applies to fetch and XMLHttpRequest. WebSocket connections are not subject to CORS preflight checks. The browser sends an Origin header during the upgrade, and a server may choose to inspect it, but there is no Access-Control-Allow-Origin dance. That is why you can open the HTML file below from your desktop, from a local dev server or from any domain and it connects without proxy tricks.
Choosing a Library: MQTT.js
MQTT.js is the de facto MQTT client for JavaScript. The same package runs in Node.js and in browsers, supports MQTT 3.1.1 and MQTT 5, and handles reconnects, keepalive and QoS 1 and 2 flows for you. Version 5 ships a prebuilt browser bundle, so you can load it from a CDN with a single script tag. The MQTT.js documentation on GitHub lists every option; the subset below is all a minimal client needs.
If you later move to a bundler or framework, the code barely changes. Our MQTT.js with Node.js guide covers the server side, and the React MQTT tutorial shows how to wrap the same client in hooks.
The Complete Single-Page MQTT JavaScript Browser Client
Save the following as mqtt-client.html and open it in any modern browser. It loads MQTT.js from a CDN, generates a unique client ID and a random topic prefix, and gives you buttons to connect, subscribe and publish.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Minimal MQTT Browser Client</title>
<script src="https://unpkg.com/mqtt@5.10.1/dist/mqtt.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
input, button { font: inherit; padding: .4rem .6rem; }
#log { border: 1px solid #ccc; height: 280px; overflow-y: auto; padding: .5rem; font-family: monospace; font-size: .85rem; }
.row { display: flex; gap: .5rem; margin: .5rem 0; }
.row input { flex: 1; }
</style>
</head>
<body>
<h1>MQTT in the Browser</h1>
<p>Status: <strong id="status">disconnected</strong></p>
<div class="row">
<input id="url" value="wss://broker.connectmq.com:8084/mqtt">
<button id="connectBtn">Connect</button>
<button id="disconnectBtn">Disconnect</button>
</div>
<div class="row">
<input id="subTopic">
<button id="subBtn">Subscribe</button>
</div>
<div class="row">
<input id="pubTopic">
<input id="pubPayload" value='{"temperature": 21.5}'>
<button id="pubBtn">Publish</button>
</div>
<div id="log"></div>
<script>
const randomId = Math.random().toString(16).slice(2, 10);
const prefix = `connectmq/demo/${randomId}`;
document.getElementById("subTopic").value = `${prefix}/#`;
document.getElementById("pubTopic").value = `${prefix}/sensors/room1`;
const statusEl = document.getElementById("status");
const logEl = document.getElementById("log");
let client = null;
function log(text) {
const line = document.createElement("div");
line.textContent = `[${new Date().toLocaleTimeString()}] ${text}`;
logEl.appendChild(line);
logEl.scrollTop = logEl.scrollHeight;
}
document.getElementById("connectBtn").onclick = () => {
if (client) return;
const url = document.getElementById("url").value;
client = mqtt.connect(url, {
clientId: `browser-${randomId}`,
protocolVersion: 5,
clean: true,
keepalive: 30,
connectTimeout: 10000,
reconnectPeriod: 3000
// For a private ConnectMQ broker add:
// username: "YOUR_USERNAME",
// password: "YOUR_PASSWORD"
});
client.on("connect", () => { statusEl.textContent = "connected"; log(`Connected to ${url}`); });
client.on("reconnect", () => { statusEl.textContent = "reconnecting"; });
client.on("close", () => { statusEl.textContent = "disconnected"; });
client.on("offline", () => log("Client went offline"));
client.on("error", (err) => log(`Error: ${err.message}`));
client.on("message", (topic, payload, packet) => {
const retained = packet.retain ? " (retained)" : "";
log(`${topic}${retained}: ${payload.toString()}`);
});
};
document.getElementById("disconnectBtn").onclick = () => {
if (!client) return;
client.end(false, () => log("Disconnected cleanly"));
client = null;
};
document.getElementById("subBtn").onclick = () => {
if (!client) return log("Connect first");
const topic = document.getElementById("subTopic").value;
client.subscribe(topic, { qos: 1 }, (err, granted) => {
if (err) return log(`Subscribe failed: ${err.message}`);
granted.forEach(g => log(`Subscribed to ${g.topic} (granted QoS ${g.qos})`));
});
};
document.getElementById("pubBtn").onclick = () => {
if (!client) return log("Connect first");
const topic = document.getElementById("pubTopic").value;
const payload = document.getElementById("pubPayload").value;
client.publish(topic, payload, { qos: 1 }, (err) => {
if (err) log(`Publish failed: ${err.message}`);
});
};
</script>
</body>
</html>
Click Connect, then Subscribe, then Publish. The message travels from your page to the broker and straight back to the same page because you are subscribed to prefix/#, which matches the publish topic.
How the Code Works
Connecting
mqtt.connect() returns a client immediately and connects in the background. The events tell you what happens next: connect after a successful CONNACK, reconnect each time it retries, close when the socket closes, and error for problems like a rejected login. Setting protocolVersion: 5 gives you MQTT 5 features such as reason codes and user properties; remove it to fall back to 3.1.1.
The client ID is generated per page load. The public broker accepts any non-empty client ID, but two clients with the same ID kick each other off in a loop, which looks like a flapping connection. Random IDs avoid that when you open the page in two tabs.
Subscribing and receiving
The message event fires for every incoming PUBLISH. payload is a byte buffer, so call toString() for text or JSON.parse(payload.toString()) for JSON. The third argument is the raw packet, which exposes retain, qos and, under MQTT 5, properties.
Notice the use of textContent in the log function. Anyone can publish to the public area of the broker, so treat every payload as untrusted input. Inserting it with innerHTML would let a malicious message run script in your page.
Publishing
client.publish(topic, payload, options, callback) queues the message. With QoS 1 the callback runs after the broker acknowledges it with PUBACK. If the connection is down, MQTT.js keeps QoS 1 and 2 messages in its outgoing store and sends them after reconnecting, as long as the page stays open. The QoS levels guide explains when that guarantee matters.
Testing From the Other Side
To prove that messages really go through the broker, watch them from a terminal while you click Publish in the page. Replace the prefix with the one your page shows:
mosquitto_sub -h broker.connectmq.com -p 1883 -i cli-watch-7f3a -t 'connectmq/demo/YOUR_PREFIX/#' -v
Publish from the terminal and the browser log updates instantly:
mosquitto_pub -h broker.connectmq.com -p 1883 -i cli-pub-7f3a -t 'connectmq/demo/YOUR_PREFIX/sensors/room1' -m '{"temperature": 23.1}'
This round trip, browser to TCP client and back, is the clearest demonstration that a WebSocket client and a native TCP client share the same topic space on the broker.
Troubleshooting Checklist
When the status never reaches "connected", work through this list in order:
- Wrong path. ConnectMQ serves WebSocket on
/mqtt. A URL without the path fails during the HTTP upgrade. - Mixed content. An
https://page cannot openws://. Usewss://on port 8084. - Port blocked. Some corporate networks only allow 443. Test from another network or a phone hotspot to rule this out.
- Duplicate client ID. If the log shows connect and close repeating every few seconds, another client is using the same ID.
- Empty or invalid credentials. On a private broker, a bad username or password produces an
errorevent with "Not authorized" or "Bad username or password". Check the values in the portal. - Public broker limits. The public broker allows 10 messages per second per client, 10 subscriptions per client, 64 KB per message and 8 topic levels. A button wired to a fast loop will hit the rate limit.
- DevTools. Open the Network tab, filter by WS and inspect the frames. You will see the binary MQTT packets and the upgrade response code.
Moving From the Public Broker to a Private One
The public broker is perfect for this tutorial, but every topic in its public area is readable by anyone who guesses it. For real projects, create your own broker:
- Sign up at app.connectmq.com and choose the free Serverless plan.
- Create a broker and copy the generated username, password and client ID.
- Change the URL to your broker's secure WebSocket endpoint on port 8084 with path
/mqtt, and setusername,passwordandclientIdin the options.
Be careful about where those credentials live. Anything you put in browser JavaScript can be read by anyone who opens DevTools. Good patterns are:
- Use separate, low-privilege credentials for browser dashboards, never the ones your devices use.
- Ask the user to type credentials at runtime instead of hard-coding them in the page.
- For public-facing sites, let a backend do the privileged publishing and keep the browser read-only. The Next.js MQTT patterns article shows that split.
The MQTT security best practices guide goes deeper into credential handling and topic design.
Where to Go Next
This single page is a foundation you can grow in a few directions:
- Live charts. Parse JSON payloads and push values into a charting library. Keep a fixed-length array so memory stays flat on long sessions.
- Chat and presence. Retained messages and Last Will make it easy to show who is online. The MQTT chat app tutorial builds exactly that.
- Typed code. If the page grows, move to TypeScript and a bundler; the MQTT with TypeScript guide covers typing payloads and events.
- Other languages. The MQTT client libraries overview compares MQTT.js with Python, Go, Java and C# clients if your backend uses a different stack.
Get Started with ConnectMQ
ConnectMQ gives you a fully managed MQTT broker with secure WebSocket on port 8084 already configured, a Let's Encrypt certificate, and an in-browser MQTT client and live topic explorer in the portal for checking what your page publishes. Start on the public MQTT test broker today, then create a private cloud MQTT broker when you are ready. Create a free account on the Serverless plan with no credit card required.
Frequently Asked Questions
Can a browser connect to MQTT on port 1883?
No. Browsers cannot open raw TCP connections, so they must use MQTT over WebSocket. On ConnectMQ that is port 8083 for ws:// and port 8084 for wss://, both on the /mqtt path.
Do I need a backend or proxy to use MQTT in the browser?
No. The page connects directly to the broker over WebSocket, and WebSocket is not subject to CORS preflight. A backend is only useful when you want to keep privileged credentials off the client.
Is MQTT.js the only JavaScript MQTT library for browsers?
It is the most widely used and actively maintained option, with MQTT 5 support and a ready browser bundle. The older Paho JavaScript client still works but is no longer the common choice for new projects.
Why does my browser client keep reconnecting every few seconds?
The most common cause is two clients sharing the same client ID, which makes the broker disconnect the older one each time. Generate a random ID per tab, and also check that you are not exceeding the public broker's rate limits.



