Ejecutar MQTT en el navegador con JavaScript es la forma más rápida de crear un dashboard en vivo, un panel de control remoto o una herramienta de depuración sin escribir un backend. En este tutorial crearás una página HTML autocontenida que se conecta al servidor MQTT público gratuito broker.connectmq.com mediante WebSocket seguro, se suscribe a un topic, publica mensajes y los muestra en tiempo real. También aprenderás por qué los navegadores necesitan WebSocket para MQTT, por qué CORS no es un problema y cómo llevar la misma página a un broker privado de forma segura.
Por qué un navegador necesita MQTT sobre WebSocket
Una página web no puede abrir sockets TCP directos. El sandbox del navegador solo permite a JavaScript comunicarse con la red mediante HTTP(S), WebSocket, WebRTC y algunas APIs más recientes. El MQTT estándar funciona sobre TCP en el puerto 1883 (o TLS en el 8883), así que un navegador no puede llegar directamente a esos puertos.
La especificación de MQTT lo resuelve permitiendo que los paquetes MQTT viajen dentro de frames binarios de WebSocket. El broker expone un listener adicional, el navegador abre un WebSocket hacia él y, a partir de ahí, el intercambio es MQTT normal: CONNECT, SUBSCRIBE, PUBLISH, PINGREQ. Si quieres el detalle a nivel de protocolo, nuestra guía de MQTT sobre WebSocket recorre el handshake y la cabecera de subprotocolo.
ConnectMQ expone dos listeners WebSocket en cada broker:
| Endpoint | URL | Cuándo usarlo |
|---|---|---|
| WebSocket | ws://broker.connectmq.com:8083/mqtt |
Solo pruebas locales desde una página http:// |
| WebSocket seguro | wss://broker.connectmq.com:8084/mqtt |
Cualquier página servida por https:// y todo uso en producción |
Usa wss:// por defecto. Una página cargada por HTTPS no puede abrir una conexión ws:// sin cifrar; el navegador la bloquea como contenido mixto antes de que salga ningún paquete de la máquina.
¿Y CORS?
CORS (Cross-Origin Resource Sharing) se aplica a fetch y XMLHttpRequest. Las conexiones WebSocket no están sujetas a las comprobaciones preflight de CORS. El navegador envía una cabecera Origin durante el upgrade y el servidor puede decidir inspeccionarla, pero no hay ningún baile de Access-Control-Allow-Origin. Por eso puedes abrir el archivo HTML de abajo desde tu escritorio, desde un servidor de desarrollo local o desde cualquier dominio y se conecta sin trucos de proxy.
Elegir una librería: MQTT.js
MQTT.js es el cliente MQTT de facto para JavaScript. El mismo paquete funciona en Node.js y en navegadores, admite MQTT 3.1.1 y MQTT 5, y gestiona por ti las reconexiones, el keepalive y los flujos de QoS 1 y 2. La versión 5 incluye un bundle precompilado para navegador, así que puedes cargarlo desde una CDN con una sola etiqueta script. La documentación de MQTT.js en GitHub enumera todas las opciones; el subconjunto de abajo es todo lo que necesita un cliente mínimo.
Si más adelante pasas a un bundler o a un framework, el código apenas cambia. Nuestra guía de MQTT.js con Node.js cubre el lado del servidor, y el tutorial de React con MQTT muestra cómo envolver el mismo cliente en hooks.
El cliente MQTT JavaScript completo para el navegador en una sola página
Guarda lo siguiente como mqtt-client.html y ábrelo en cualquier navegador moderno. Carga MQTT.js desde una CDN, genera un client ID único y un prefijo de topic aleatorio, y te da botones para conectar, suscribirte y publicar.
<!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
// Para un broker privado de ConnectMQ añade:
// 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>
Pulsa Connect, después Subscribe y después Publish. El mensaje viaja desde tu página al broker y vuelve directamente a la misma página, porque estás suscrito a prefix/#, que coincide con el topic de publicación.
Cómo funciona el código
Conexión
mqtt.connect() devuelve un cliente de inmediato y se conecta en segundo plano. Los eventos te dicen qué ocurre después: connect tras un CONNACK correcto, reconnect cada vez que reintenta, close cuando se cierra el socket y error para problemas como un login rechazado. Establecer protocolVersion: 5 te da funciones de MQTT 5 como los reason codes y las propiedades de usuario; quítalo para volver a 3.1.1.
El client ID se genera en cada carga de la página. El broker público acepta cualquier client ID no vacío, pero dos clientes con el mismo ID se expulsan mutuamente en bucle, lo que parece una conexión inestable. Los IDs aleatorios lo evitan cuando abres la página en dos pestañas.
Suscripción y recepción
El evento message se dispara con cada PUBLISH entrante. payload es un búfer de bytes, así que llama a toString() para texto o a JSON.parse(payload.toString()) para JSON. El tercer argumento es el paquete en bruto, que expone retain, qos y, con MQTT 5, properties.
Fíjate en el uso de textContent en la función log. Cualquiera puede publicar en el área pública del broker, así que trata cada payload como una entrada no fiable. Insertarlo con innerHTML permitiría que un mensaje malicioso ejecutara scripts en tu página.
Publicación
client.publish(topic, payload, options, callback) pone el mensaje en cola. Con QoS 1 el callback se ejecuta después de que el broker lo confirme con un PUBACK. Si la conexión está caída, MQTT.js guarda los mensajes QoS 1 y 2 en su almacén de salida y los envía tras reconectar, siempre que la página siga abierta. La guía de niveles de QoS explica cuándo importa esa garantía.
Probar desde el otro lado
Para demostrar que los mensajes pasan realmente por el broker, obsérvalos desde un terminal mientras pulsas Publish en la página. Sustituye el prefijo por el que muestra tu página:
mosquitto_sub -h broker.connectmq.com -p 1883 -i cli-watch-7f3a -t 'connectmq/demo/YOUR_PREFIX/#' -v
Publica desde el terminal y el log del navegador se actualiza al instante:
mosquitto_pub -h broker.connectmq.com -p 1883 -i cli-pub-7f3a -t 'connectmq/demo/YOUR_PREFIX/sensors/room1' -m '{"temperature": 23.1}'
Este viaje de ida y vuelta, del navegador a un cliente TCP y de vuelta, es la demostración más clara de que un cliente WebSocket y un cliente TCP nativo comparten el mismo espacio de topics en el broker.
Lista de comprobación para resolver problemas
Cuando el estado nunca llega a "connected", repasa esta lista en orden:
- Ruta incorrecta. ConnectMQ sirve WebSocket en
/mqtt. Una URL sin la ruta falla durante el upgrade HTTP. - Contenido mixto. Una página
https://no puede abrirws://. Usawss://en el puerto 8084. - Puerto bloqueado. Algunas redes corporativas solo permiten el 443. Prueba desde otra red o desde la zona wifi de un móvil para descartarlo.
- Client ID duplicado. Si el log muestra conexiones y cierres que se repiten cada pocos segundos, otro cliente está usando el mismo ID.
- Credenciales vacías o no válidas. En un broker privado, un usuario o contraseña incorrectos producen un evento
errorcon "Not authorized" o "Bad username or password". Comprueba los valores en el portal. - Límites del broker público. El broker público permite 10 mensajes por segundo por cliente, 10 suscripciones por cliente, 64 KB por mensaje y 8 niveles de topic. Un botón conectado a un bucle rápido alcanzará el límite de frecuencia.
- DevTools. Abre la pestaña Network, filtra por WS e inspecciona los frames. Verás los paquetes MQTT binarios y el código de respuesta del upgrade.
Pasar del broker público a uno privado
El broker público es perfecto para este tutorial, pero cualquier topic de su área pública lo puede leer quien lo adivine. Para proyectos reales, crea tu propio broker:
- Regístrate en app.connectmq.com y elige el plan Serverless gratuito.
- Crea un broker y copia el usuario, la contraseña y el client ID generados.
- Cambia la URL al endpoint WebSocket seguro de tu broker en el puerto 8084 con la ruta
/mqtt, y estableceusername,passwordyclientIden las opciones.
Ten cuidado con dónde viven esas credenciales. Cualquier cosa que pongas en el JavaScript del navegador la puede leer quien abra DevTools. Buenos patrones son:
- Usa credenciales separadas y con pocos privilegios para los dashboards del navegador, nunca las que usan tus dispositivos.
- Pide al usuario que escriba las credenciales en tiempo de ejecución en lugar de incrustarlas en la página.
- Para sitios públicos, deja que un backend haga las publicaciones privilegiadas y mantén el navegador en solo lectura. El artículo de patrones de MQTT con Next.js muestra esa separación.
La guía de buenas prácticas de seguridad en MQTT profundiza en la gestión de credenciales y el diseño de topics.
Hacia dónde seguir
Esta única página es una base que puedes hacer crecer en varias direcciones:
- Gráficos en vivo. Parsea los payloads JSON y envía los valores a una librería de gráficos. Mantén un array de longitud fija para que la memoria se mantenga estable en sesiones largas.
- Chat y presencia. Los retained messages y el Last Will facilitan mostrar quién está conectado. El tutorial de app de chat con MQTT construye exactamente eso.
- Código tipado. Si la página crece, pasa a TypeScript y a un bundler; la guía de MQTT con TypeScript cubre el tipado de payloads y eventos.
- Otros lenguajes. La visión general de librerías cliente MQTT compara MQTT.js con clientes de Python, Go, Java y C# si tu backend usa otro stack.
Empieza con ConnectMQ
ConnectMQ te ofrece un broker MQTT gestionado con WebSocket seguro en el puerto 8084 ya configurado, un certificado de Let's Encrypt, y un cliente MQTT en el navegador y un explorador de topics en vivo en el portal para comprobar lo que publica tu página. Empieza hoy en el servidor público, solo para pruebas, y crea un broker MQTT en la nube privado cuando estés listo. Crea una cuenta gratuita en el plan Serverless sin necesidad de tarjeta de crédito.
Preguntas frecuentes
¿Puede un navegador conectarse a MQTT en el puerto 1883?
No. Los navegadores no pueden abrir conexiones TCP directas, así que deben usar MQTT sobre WebSocket. En ConnectMQ eso es el puerto 8083 para ws:// y el 8084 para wss://, ambos en la ruta /mqtt.
¿Necesito un backend o un proxy para usar MQTT en el navegador?
No. La página se conecta directamente al broker por WebSocket, y WebSocket no está sujeto al preflight de CORS. Un backend solo es útil cuando quieres mantener las credenciales privilegiadas fuera del cliente.
¿Es MQTT.js la única librería MQTT de JavaScript para navegadores?
Es la opción más usada y con mantenimiento más activo, con soporte de MQTT 5 y un bundle listo para navegador. El antiguo cliente JavaScript de Paho sigue funcionando, pero ya no es la elección habitual para proyectos nuevos.
¿Por qué mi cliente en el navegador se reconecta cada pocos segundos?
La causa más común es que dos clientes comparten el mismo client ID, lo que hace que el broker desconecte al más antiguo cada vez. Genera un ID aleatorio por pestaña y comprueba también que no superas los límites de frecuencia del broker público.



