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/+/temperature or plant/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.

What you build vs what MQTT provides MQTT over WebSockets provides topic routing with wildcards, QoS delivery levels, retained current values and last-will offline detection that a raw WebSocket protocol must build itself, but request-response patterns and server-side business logic fit less naturally. What you build vs what MQTT provides Raw WebSocket MQTT over WebSocket Subscriptions + routing your protocol topics + wildcards Delivery guarantees your acks QoS 0 / 1 / 2 Current value on subscribe your snapshot retained messages Offline detection your heartbeat + presence keepalive + last will Request/response natural awkward (v5 helps) Server-side business logic in your handlers outside the broker MQTT excels at pub/sub of device and telemetry data; it is awkward for RPC-heavy apps
MQTT is a finished pub/sub protocol; raw WebSockets are a blank 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.

Retained values and last will in practice A device connects with a retained last-will of offline and publishes a retained online status; a dashboard subscribing later immediately receives the retained online status; when the device loses power without disconnecting, the broker publishes the will and the dashboard sees it go offline. Retained values and last will in practice Device line 3 Broker Dashboard CONNECT, will: status=offline (retained) PUBLISH status=online (retained) SUBSCRIBE plant/7/line/+/status retained: line 3 online power loss, no DISCONNECT will: line 3 offline Snapshot and offline detection come from the protocol, not from code you write
Retained messages and wills replace a snapshot endpoint and a presence service.

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.

Illustrative build vs operate for an IoT dashboard A custom WebSocket protocol means building about six components — routing, acknowledgements, snapshots, presence, reconnect handling and the relay service — and operating two, while MQTT over WebSockets means building one client integration and operating one broker. Illustrative build vs operate for an IoT dashboard routing, acks, snapshots, presence, reconnect, broker/service components you build components you operate 0 2 4 6 2 Raw WS: custom protocol 1 1 MQTT over WS
For device and telemetry data, MQTT removes most of the protocol you would otherwise write.

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.

Back to WebSocket vs SSE vs WebRTC.