Na maioria dos runtimes edge não existem sockets TCP brutos nem a pilha de rede do Node.js, então as bibliotecas cliente MQTT de sempre não rodam ali. O que existe é a API WebSocket — e o MQTT funciona muito bem sobre WebSockets. Um Cloudflare Worker pode abrir wss://broker.connectmq.com:8084/mqtt, enviar um CONNECT, publicar e fechar, tudo dentro de uma requisição. Na Vercel, o runtime Node.js roda um cliente MQTT normal, então o truque edge só é necessário em runtimes exclusivamente edge.
Este tutorial monta um publisher MQTT sem dependências para Cloudflare Workers, explica o ctx.waitUntil, lista os limites de plataforma que importam e mostra como verificar cada mensagem no navegador.
O que runtimes edge podem e não podem fazer
| Necessidade | Cloudflare Workers | Vercel (runtime Node.js) | Runtime Edge da Vercel |
|---|---|---|---|
| WebSocket de saída | Sim, via fetch com Upgrade: websocket | Sim, via biblioteca | Limitado — confira a documentação atual |
| TCP / TLS de saída | Sim, via connect() de cloudflare:sockets | Sim (net / tls do Node) | Não |
| Rodar o MQTT.js sem mudanças | Não suportado | Sim | Não |
| Manter uma assinatura viva | Só durante uma requisição (Durable Objects para mais tempo) | Só durante a execução da função | Só durante uma requisição |
A regra prática: funções edge são publishers, não subscribers. Elas reagem a uma requisição HTTP, emitem uma ou poucas mensagens MQTT e terminam. Tudo o que precisa escutar continuamente pertence a um serviço de longa duração.
Se você está na Vercel, a resposta mais simples é usar o runtime Node.js na rota que fala MQTT e tratá-la como qualquer outra função serverless — os padrões de reaproveitamento de conexão de publicar a partir do AWS Lambda se aplicam diretamente. Hoje a própria Vercel direciona a maioria das cargas para o runtime Node.js.
Um cliente MQTT mínimo sobre WebSocket
Publicar com QoS 0 exige só três pacotes: CONNECT, PUBLISH e DISCONNECT, além de ler o CONNACK. O layout dos bytes é simples — um cabeçalho fixo, um campo "remaining length" de tamanho variável e strings UTF-8 com prefixo de tamanho (veja a estrutura de pacotes MQTT para os detalhes).
const enc = new TextEncoder();
// String UTF-8 com prefixo de tamanho de 2 bytes big-endian
const str = (s) => { const b = enc.encode(s); return [b.length >> 8, b.length & 0xff, ...b]; };
// "remaining length" de tamanho variável do 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 do 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');
}Alguns detalhes são fáceis de errar:
- O cabeçalho de subprotocolo. A maioria dos brokers só aceita o upgrade se o cliente oferecer
Sec-WebSocket-Protocol: mqtt. Sem ele, normalmente você recebe um HTTP 400 ou o socket fecha na hora. - O caminho. O listener WebSocket do ConnectMQ fica em
/mqtt; um caminho errado retorna HTTP 404 (WebSocket path 404). - Client IDs aleatórios. Centenas de localizações edge podem rodar seu Worker no mesmo instante; um ID fixo faz com que elas se derrubem.
- Frames binários. MQTT sobre WebSockets usa mensagens binárias, por isso o código envia
Uint8Array.
Se você precisa de confirmação de entrega, estenda o publishPacket para QoS 1 (cabeçalho 0x32 mais um packet identifier de dois bytes depois do topic) e espere o PUBACK (0x40) antes de fechar. Mantenha os payloads pequenos: a calculadora de tamanho de pacote mostra os bytes exatos na rede.
O Worker
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 na hora; conclui o publish MQTT em 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');
},
};name = "edge-mqtt"
main = "src/index.js"
compatibility_date = "2026-10-01"
[vars]
MQTT_WS_URL = "https://broker.connectmq.com:8084/mqtt"Guarde as credenciais como secrets, nunca no wrangler.toml: npx wrangler secret put MQTT_USER e npx wrangler secret put MQTT_PASS. Repare que a URL usa https:// — o fetch dos Workers faz o upgrade para WebSocket quando vê o cabeçalho Upgrade.
O ctx.waitUntil mantém o Worker vivo depois do envio da resposta até a promise ser resolvida, então o visitante não espera o handshake com o broker. Se quem chama precisa saber se o publish deu certo, faça await publishMqtt(...) antes de responder e retorne 502 em caso de falha.
TCP bruto com cloudflare:sockets
Os Cloudflare Workers também podem abrir conexões TCP de saída com connect() de cloudflare:sockets, incluindo TLS via secureTransport: "on". As funções de pacote acima funcionam sem mudanças — escreva em socket.writable em vez de ws.send e leia o CONNACK de socket.readable. Assim você pode usar a porta 8883 em vez de WebSockets. Ainda assim, WebSockets continua sendo a escolha mais portável, porque o mesmo código roda em outras plataformas edge e em navegadores (MQTT sobre WebSockets).
Limites a considerar no design
Os números exatos variam por plataforma e plano e mudam com o tempo, então consulte a documentação atual. As categorias que importam para MQTT são:
- Tempo de CPU e tempo total por requisição. Um handshake TLS + WebSocket + MQTT custa várias idas e voltas na rede; publique um lote por requisição em vez de uma conexão por mensagem.
- Nenhum reaproveitamento de conexão confiável. Isolates podem ser reutilizados, mas trate cada requisição como uma conexão nova.
- Limites de conexões de saída e subrequests. Cada WebSocket conta nesses limites.
- Portas de saída. Algumas plataformas restringem as portas que requisições de saída podem usar. Se a 8084 não for permitida, use um listener do broker na 443 (MQTT na porta 443).
- Taxa de conexões no broker. Um pico de tráfego no seu site vira um pico de conexões MQTT. Para eventos de alto volume, considere enviá-los a um único serviço agregador que mantém uma conexão aberta.
Erros comuns
| Erro | Causa | Correção |
|---|---|---|
WebSocket upgrade failed: HTTP 404 | Caminho errado | Use /mqtt |
WebSocket upgrade failed: HTTP 400 ou fechamento imediato | Falta o subprotocolo mqtt ou http:// simples em uma porta TLS | Envie Sec-WebSocket-Protocol: mqtt, use https:// com a 8084 |
CONNACK return code 4 | Usuário ou senha incorretos | Confira os secrets do Worker |
CONNACK return code 5 | Não autorizado (ACL ou conta) | Permita que o cliente conecte e publique |
CONNACK timeout | Porta bloqueada ou broker inacessível | Teste o endpoint antes no cliente online |
Outras falhas específicas de WebSocket estão em WebSocket connection failed.
Testando
- Abra o cliente MQTT online, conecte em
broker.connectmq.compor WebSockets seguros e assinetestmqtt/edge/#. - Rode o Worker localmente com
npx wrangler deve chame-o:curl "http://localhost:8787/checkout?device=kiosk-1". - Uma mensagem com o nome do dispositivo e o timestamp deve aparecer no navegador. Se não aparecer, procure no console do
wrangler deva mensagem de erro depublishMqtt. - Faça o deploy com
npx wrangler deploye repita contra a URLworkers.dev; o campocoloagora mostra qual localização edge publicou a mensagem.
Veja as mensagens edge chegando no navegador
Funções edge e MQTT se encontram no WebSocket: umas cinquenta linhas de codificação de pacotes transformam qualquer runtime edge em um publisher MQTT sem dependências. Mantenha as funções como publishers de vida curta, proteja as credenciais com ACLs restritas e deixe as assinaturas de longa duração para serviços que conseguem ficar conectados.
Perguntas frequentes
Posso usar o MQTT.js dentro de um Cloudflare Worker?
Não é um ambiente suportado. O MQTT.js espera streams e sockets do Node.js; com a compatibilidade com Node ativada, parte pode funcionar, mas o comportamento pode mudar entre versões do runtime. Um pequeno codificador MQTT sobre a API WebSocket dos Workers é mais previsível para publicar.
Uma função edge pode assinar topics MQTT?
Só durante uma requisição. Funções edge são presas à requisição e não conseguem manter uma conexão aberta indefinidamente. Para assinaturas de longa duração, use um serviço sempre ligado ou uma primitiva feita para isso, como os Durable Objects da Cloudflare, e confira os limites.
Qual porta do broker uma função edge deve usar?
WebSockets seguros (wss, porta 8084 no ConnectMQ, caminho /mqtt) é a opção mais portável, porque todo runtime edge suporta WebSockets. Se a sua plataforma restringe portas de saída, use um listener do broker na 443.


