Tutoriales

MQTT desde funciones edge (Cloudflare Workers, Vercel)

Publica mensajes MQTT desde Cloudflare Workers y funciones edge con WebSockets seguros: un cliente sin dependencias, waitUntil, límites de plataforma y pruebas.

Actualizado · 9 min de lectura

MQTT desde funciones edge (Cloudflare Workers, Vercel)

En la mayoría de runtimes edge no hay sockets TCP en crudo ni pila de red de Node.js, así que las librerías cliente MQTT habituales no funcionan ahí. Lo que sí hay es la API WebSocket, y MQTT funciona perfectamente sobre WebSockets. Un Cloudflare Worker puede abrir wss://broker.connectmq.com:8084/mqtt, enviar un CONNECT, publicar y cerrar, todo dentro de una petición. En Vercel, el runtime de Node.js ejecuta un cliente MQTT normal, así que el truco edge solo hace falta en runtimes exclusivamente edge.

Este tutorial construye un publisher MQTT sin dependencias para Cloudflare Workers, explica ctx.waitUntil, enumera los límites de plataforma que importan y muestra cómo verificar cada mensaje en tu navegador.

Qué pueden y qué no pueden hacer los runtimes edge

NecesidadCloudflare WorkersVercel (runtime Node.js)Runtime Edge de Vercel
WebSocket salienteSí, con fetch y Upgrade: websocketSí, con una libreríaLimitado: revisa la documentación actual
TCP / TLS salienteSí, con connect() de cloudflare:socketsSí (net / tls de Node)No
Ejecutar MQTT.js sin cambiosNo soportadoSíNo
Mantener viva una suscripciónSolo durante una petición (Durable Objects para más)Solo durante la duración de la funciónSolo durante una petición

La regla práctica: las funciones edge son publishers, no subscribers. Reaccionan a una petición HTTP, emiten uno o unos pocos mensajes MQTT y terminan. Todo lo que tenga que escuchar de forma continua pertenece a un servicio de larga duración.

Si estás en Vercel, la respuesta más sencilla es usar el runtime de Node.js para la ruta que habla MQTT y tratarla como cualquier otra función serverless: los patrones de reutilización de conexión de publicar desde AWS Lambda se aplican directamente. Hoy la propia Vercel orienta la mayoría de cargas de trabajo hacia el runtime de Node.js.

Un cliente MQTT mínimo sobre WebSocket

Publicar con QoS 0 solo requiere tres paquetes: CONNECT, PUBLISH y DISCONNECT, además de leer el CONNACK. La estructura de bytes es sencilla: una cabecera fija, un campo «remaining length» de longitud variable y cadenas UTF-8 con prefijo de longitud (consulta la estructura de paquetes MQTT para los detalles).

src/mqtt.js
const enc = new TextEncoder();

// Cadena UTF-8 con un prefijo de longitud de 2 bytes big-endian
const str = (s) => { const b = enc.encode(s); return [b.length >> 8, b.length & 0xff, ...b]; };

// "remaining length" de longitud variable de MQTT
function remLen(n) {
  const out = [];
  do {
    let digit = n % 128;
    n = Math.floor(n / 128);
    if (n > 0) digit |= 0x80;
    out.push(digit);
  } while (n > 0);
  return out;
}

const packet = (header, body) => new Uint8Array([header, ...remLen(body.length), ...body]);

function connectPacket(clientId, username, password, keepAlive = 30) {
  let flags = 0x02;                       // clean session
  if (username) flags |= 0x80;
  if (password) flags |= 0x40;
  const body = [...str('MQTT'), 4, flags, keepAlive >> 8, keepAlive & 0xff, ...str(clientId)];
  if (username) body.push(...str(username));
  if (password) body.push(...str(password));
  return packet(0x10, body);              // CONNECT de MQTT 3.1.1
}

const publishPacket = (topic, payload) => packet(0x30, [...str(topic), ...enc.encode(payload)]);
const DISCONNECT = new Uint8Array([0xe0, 0x00]);

export async function publishMqtt(env, topic, payload) {
  const resp = await fetch(env.MQTT_WS_URL, {
    headers: { Upgrade: 'websocket', 'Sec-WebSocket-Protocol': 'mqtt' },
  });
  const ws = resp.webSocket;
  if (!ws) throw new Error(`WebSocket upgrade failed: HTTP ${resp.status}`);
  ws.accept();

  const connack = new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error('CONNACK timeout')), 5000);
    ws.addEventListener('message', (ev) => {
      const b = new Uint8Array(ev.data);
      if (b[0] !== 0x20) return;
      clearTimeout(timer);
      b[3] === 0 ? resolve() : reject(new Error(`CONNACK return code ${b[3]}`));
    });
    ws.addEventListener('close', () => reject(new Error('socket closed before CONNACK')));
  });

  ws.send(connectPacket(`edge-${crypto.randomUUID().slice(0, 8)}`, env.MQTT_USER, env.MQTT_PASS));
  await connack;
  ws.send(publishPacket(topic, payload));
  ws.send(DISCONNECT);
  ws.close(1000, 'done');
}

Hay detalles en los que es fácil equivocarse:

  • La cabecera de subprotocolo. La mayoría de brokers solo aceptan el upgrade si el cliente ofrece Sec-WebSocket-Protocol: mqtt. Sin ella sueles recibir un HTTP 400 o el socket se cierra al instante.
  • La ruta. El listener WebSocket de ConnectMQ está en /mqtt; una ruta incorrecta devuelve HTTP 404 (WebSocket path 404).
  • Client IDs aleatorios. Cientos de ubicaciones edge pueden ejecutar tu Worker en el mismo momento; un ID fijo hace que se desconecten entre sí.
  • Frames binarios. MQTT sobre WebSockets usa mensajes binarios, por eso el código envía Uint8Array.

Si necesitas confirmación de entrega, amplía publishPacket a QoS 1 (cabecera 0x32 más un packet identifier de dos bytes tras el topic) y espera el PUBACK (0x40) antes de cerrar. Mantén los payloads pequeños: la calculadora de tamaño de paquete muestra los bytes exactos en la red.

El Worker

src/index.js
import { publishMqtt } from './mqtt.js';

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const device = url.searchParams.get('device') ?? 'demo';
    const message = JSON.stringify({ device, path: url.pathname, colo: request.cf?.colo, ts: Date.now() });

    // Responde enseguida; termina el publish MQTT en segundo plano
    ctx.waitUntil(
      publishMqtt(env, `testmqtt/edge/${device}/hits`, message)
        .catch((err) => console.error('mqtt publish failed:', err.message)),
    );
    return new Response('queued\n');
  },
};
wrangler.toml
name = "edge-mqtt"
main = "src/index.js"
compatibility_date = "2026-10-01"

[vars]
MQTT_WS_URL = "https://broker.connectmq.com:8084/mqtt"

Guarda las credenciales como secrets, nunca en wrangler.toml: npx wrangler secret put MQTT_USER y npx wrangler secret put MQTT_PASS. Fíjate en que la URL usa https://: el fetch de Workers hace el upgrade a WebSocket cuando ve la cabecera Upgrade.

ctx.waitUntil mantiene vivo el Worker después de enviar la respuesta hasta que la promesa se resuelve, así el visitante no espera al handshake con el broker. Si quien llama necesita saber si el publish tuvo éxito, haz await publishMqtt(...) antes de responder y devuelve un 502 si falla.

TCP en crudo con cloudflare:sockets

Cloudflare Workers también puede abrir conexiones TCP salientes con connect() de cloudflare:sockets, incluido TLS con secureTransport: "on". Las funciones de paquetes de arriba funcionan sin cambios: escríbelas en socket.writable en lugar de ws.send y lee el CONNACK de socket.readable. Así puedes usar el puerto 8883 en lugar de WebSockets. Aun así, WebSockets sigue siendo la opción más portable, porque el mismo código funciona en otras plataformas edge y en navegadores (MQTT sobre WebSockets).

Límites a tener en cuenta en el diseño

Los números exactos varían según la plataforma y el plan, y cambian con el tiempo, así que consulta la documentación actual. Las categorías que importan para MQTT son:

  • Tiempo de CPU y de reloj por petición. Un handshake TLS + WebSocket + MQTT cuesta varios viajes de ida y vuelta; publica un lote por petición en vez de una conexión por mensaje.
  • No hay reutilización de conexiones en la que confiar. Los isolates pueden reutilizarse, pero trata cada petición como una conexión nueva.
  • Límites de conexiones salientes y subrequests. Cada WebSocket cuenta para ellos.
  • Puertos de salida. Algunas plataformas restringen los puertos que pueden usar las peticiones salientes. Si el 8084 no está permitido, usa un listener del broker en el 443 (MQTT en el puerto 443).
  • Tasa de conexiones en el broker. Un pico de tráfico en tu web se convierte en un pico de conexiones MQTT. Para eventos de gran volumen, plantéate enviarlos a un único servicio agregador que mantenga una sola conexión abierta.

Errores habituales

ErrorCausaSolución
WebSocket upgrade failed: HTTP 404Ruta incorrectaUsa /mqtt
WebSocket upgrade failed: HTTP 400 o cierre inmediatoFalta el subprotocolo mqtt o http:// plano contra un puerto TLSEnvía Sec-WebSocket-Protocol: mqtt, usa https:// con el 8084
CONNACK return code 4Usuario o contraseña incorrectosRevisa los secrets del Worker
CONNACK return code 5No autorizado (ACL o cuenta)Permite al cliente conectarse y publicar
CONNACK timeoutPuerto bloqueado o broker inaccesiblePrueba antes el endpoint desde el cliente online

Otros fallos específicos de WebSocket se tratan en WebSocket connection failed.

Probarlo

  1. Abre el cliente MQTT online, conéctate a broker.connectmq.com por WebSockets seguros y suscríbete a testmqtt/edge/#.
  2. Ejecuta el Worker en local con npx wrangler dev y llámalo: curl "http://localhost:8787/checkout?device=kiosk-1".
  3. Debería aparecer en el navegador un mensaje con el nombre del dispositivo y la marca de tiempo. Si no aparece, revisa en la consola de wrangler dev el mensaje de error de publishMqtt.
  4. Despliega con npx wrangler deploy y repite contra la URL workers.dev; el campo colo muestra ahora qué ubicación edge publicó el mensaje.

Mira llegar los mensajes edge a tu navegador

Las funciones edge y MQTT se encuentran en el WebSocket: unas cincuenta líneas de codificación de paquetes convierten cualquier runtime edge en un publisher MQTT sin dependencias. Mantén las funciones como publishers de vida corta, protege sus credenciales con ACLs estrictas y deja las suscripciones de larga duración a servicios que puedan permanecer conectados.

Preguntas frecuentes

¿Puedo usar MQTT.js dentro de un Cloudflare Worker?

No es un entorno soportado. MQTT.js espera streams y sockets de Node.js; con la compatibilidad con Node activada parte puede funcionar, pero el comportamiento puede cambiar entre versiones del runtime. Un pequeño codificador MQTT sobre la API WebSocket de Workers es más predecible para publicar.

¿Puede una función edge suscribirse a topics MQTT?

Solo durante una petición. Las funciones edge están ligadas a la petición y no pueden mantener una conexión abierta indefinidamente. Para suscripciones de larga duración usa un servicio siempre activo o una primitiva pensada para ello, como Cloudflare Durable Objects, y revisa sus límites.

¿Qué puerto del broker debe usar una función edge?

WebSockets seguros (wss, puerto 8084 en ConnectMQ, ruta /mqtt) es la opción más portable porque todos los runtimes edge soportan WebSockets. Si tu plataforma restringe los puertos de salida, usa un listener del broker en el 443.