MQTT over WebSockets vs raw WebSockets #
Your IoT devices already publish telemetry to an MQTT broker, and now the operations dashboard needs the same data live in the browser. One option is a custom WebSocket service that subscribes to MQTT and re-publishes in your own JSON protocol. The other is to let the browser speak MQTT directly — browsers cannot open raw TCP, but MQTT has a standard WebSocket transport, and brokers such as Mosquitto, EMQX and HiveMQ serve it. The same question arises for teams without any devices who are tempted by MQTT’s ready-made features: topic wildcards, delivery levels, retained last values and disconnect notifications. MQTT over WebSockets is a strong choice in some architectures and needless complexity in others.
Root cause #
A raw WebSocket gives you a pipe; everything else — subscriptions, routing, acknowledgements, “last known value”, presence — is protocol you design, as described in WebSocket Message Protocol Design. MQTT is such a protocol, already designed and standardized, built around a broker:
- Hierarchical topics with wildcards. Clients subscribe to
plant/7/line/+/temperatureorplant/7/#, and the broker routes each publish to matching subscribers. - Quality of service levels. QoS 0 (at most once), QoS 1 (at least once, with acknowledgements and redelivery) and QoS 2 (exactly once, via a four-step handshake).
- Retained messages. The broker stores the last message on a topic and delivers it immediately to new subscribers — a built-in “current value” snapshot.
- Last will and testament. A client registers a message the broker publishes if it disconnects uncleanly — built-in offline detection.
- Persistent sessions. With a clean-start flag off, the broker queues QoS 1/2 messages for a disconnected client and delivers them on reconnect.
Carrying MQTT over WebSockets simply wraps MQTT packets in binary WebSocket frames, negotiated with the mqtt subprotocol. The browser gets the full feature set; the price is a broker in the architecture and an MQTT client library in the page.
Resolution #
Use MQTT over WebSockets when your data is naturally pub/sub over a topic hierarchy — devices, sensors, machines, vehicles, telemetry — especially when an MQTT broker already exists. Use your own protocol over raw WebSockets when the application is request/response and business-logic heavy — collaborative editing, chat with server-side moderation, transactional workflows — where a broker would only relay messages to a service that does the real work.
A browser client using MQTT.js over WebSockets looks like this:
import mqtt from 'mqtt';
const BROKER_URL = 'wss://broker.example.com:8084/mqtt'; // broker's WebSocket listener
const KEEPALIVE_S = 30; // MQTT-level heartbeat
export function connectDashboard(token: string, onReading: (line: string, value: number) => void) {
const client = mqtt.connect(BROKER_URL, {
protocolVersion: 5,
clientId: `dash-${crypto.randomUUID()}`,
username: 'dashboard',
password: token, // short-lived token checked by the broker's auth plugin
keepalive: KEEPALIVE_S,
clean: true, // dashboards need current state, not a queued backlog
reconnectPeriod: 2_000, // MQTT.js reconnects automatically (add jitter server-side via limits)
});
client.on('connect', () => {
// '+' matches one level: every line's temperature on plant 7.
// Retained messages give the last reading for each line immediately.
client.subscribe('plant/7/line/+/temperature', { qos: 0 });
client.subscribe('plant/7/line/+/status', { qos: 1 }); // must not be missed
});
client.on('message', (topic, payload) => {
const [, , , line, metric] = topic.split('/');
if (metric === 'temperature') onReading(line, Number(payload.toString()));
});
client.on('close', () => console.info('mqtt: connection closed, reconnecting'));
return () => client.end(true);
}
On the device side, each machine registers a last will on plant/7/line/3/status with payload offline and the retained flag, then publishes online (retained) after connecting. If a device vanishes without disconnecting cleanly, the broker publishes its will, and every dashboard sees the status change — presence without writing a presence service, the problem that otherwise needs a presence system with Redis.
Security needs the same care as any WebSocket endpoint: authenticate on connect (tokens or client certificates), and enforce topic ACLs in the broker so a dashboard user can subscribe only to their plant’s topics and publish nowhere. A broker that allows any authenticated client to subscribe to # exposes everything.
Edge cases #
Broker scaling. Every browser becomes a broker client. Thousands of dashboards are fine for clustered brokers; hundreds of thousands of consumer browsers call for a broker designed for that scale, careful topic design, and load testing like any WebSocket fleet — see Load Testing & Capacity Planning.
QoS in browsers. QoS 1 and 2 add acknowledgement traffic and require the broker to track in-flight messages per client. For dashboards, QoS 0 plus retained values is usually right; reserve QoS 1 for state changes that must not be missed.
Persistent sessions and short-lived tabs. A browser client with clean: false makes the broker queue messages for it after the tab closes. With random client ids per tab, those queues are never claimed and accumulate. Use clean sessions for browsers, or stable ids with bounded expiry.
Verification #
Test the broker’s WebSocket listener and ACLs directly before building UI:
# Subscribe over WebSockets with the mqtt CLI (MQTT.js) and check ACL enforcement.
npx mqtt sub -h broker.example.com -p 8084 -l wss --path /mqtt -u dashboard -P "$TOKEN" -t 'plant/7/#' -v
npx mqtt sub -h broker.example.com -p 8084 -l wss --path /mqtt -u dashboard -P "$TOKEN" -t 'plant/8/#' -v # must be denied
Then verify the behaviours you rely on: a new subscriber receives retained values immediately; killing a device’s network (not its client) produces its will within roughly 1.5 × its keepalive; and the dashboard reconnects automatically after a broker restart.
Operational checklist #
FAQ #
Can a browser connect to an MQTT broker? #
Yes, through MQTT over WebSockets. Browsers cannot open raw TCP sockets, but MQTT defines a WebSocket transport, and most brokers expose a WebSocket listener that clients such as MQTT.js use.
Should I use MQTT instead of building my own WebSocket protocol? #
For telemetry, device and IoT data with topic hierarchies, often yes: topics, QoS, retained values and last will cover most of what you would build. For request/response-heavy applications with server-side logic, a custom protocol over raw WebSockets fits better.
Is MQTT over WebSockets slower than raw WebSockets? #
MQTT packets are compact and add little overhead; the extra cost is the broker hop and QoS acknowledgements when used. For dashboards and telemetry, latency is dominated by the network, not the protocol.
How does MQTT compare with Socket.IO? #
Both add structure on top of WebSockets. MQTT is a standardized, broker-centric pub/sub protocol with delivery levels and retained state; Socket.IO is an event framework tied to its own server. See Socket.IO vs raw WebSockets.
Related #
- Socket.IO vs Raw WebSockets — the other popular protocol layer.
- WebSocket Subprotocol Negotiation — how
mqttis negotiated. - Mutual TLS for WebSocket Clients — certificate identity for devices.
- NATS vs Redis for WebSocket Fan-Out — other brokers behind WebSocket fleets.
Back to WebSocket vs SSE vs WebRTC.