La plupart des runtimes edge n'ont ni sockets TCP bruts ni pile réseau Node.js : les bibliothèques clientes MQTT habituelles n'y tournent donc pas. En revanche, ils disposent de l'API WebSocket — et MQTT fonctionne parfaitement sur WebSockets. Un Cloudflare Worker peut ouvrir wss://broker.connectmq.com:8084/mqtt, envoyer un CONNECT, publier et fermer, le tout dans une seule requête. Sur Vercel, le runtime Node.js exécute un client MQTT normal : l'astuce edge n'est nécessaire que pour les runtimes purement edge.
Ce tutoriel construit un publisher MQTT sans dépendance pour Cloudflare Workers, explique ctx.waitUntil, liste les limites de plateforme qui comptent et montre comment vérifier chaque message dans votre navigateur.
Ce que les runtimes edge peuvent faire, ou non
| Besoin | Cloudflare Workers | Vercel (runtime Node.js) | Runtime Edge de Vercel |
|---|---|---|---|
| WebSocket sortant | Oui, via fetch avec Upgrade: websocket | Oui, via une bibliothèque | Limité — consultez la documentation actuelle |
| TCP / TLS sortant | Oui, via connect() de cloudflare:sockets | Oui (net / tls de Node) | Non |
| Exécuter MQTT.js tel quel | Non pris en charge | Oui | Non |
| Garder un abonnement actif | Seulement pendant une requête (Durable Objects pour plus longtemps) | Seulement pendant l'exécution de la fonction | Seulement pendant une requête |
La règle pratique : les fonctions edge sont des publishers, pas des subscribers. Elles réagissent à une requête HTTP, émettent un ou quelques messages MQTT et se terminent. Tout ce qui doit écouter en continu relève d'un service longue durée.
Sur Vercel, le plus simple est d'utiliser le runtime Node.js pour la route qui parle MQTT et de la traiter comme n'importe quelle fonction serverless — les schémas de réutilisation de connexion de publier depuis AWS Lambda s'appliquent directement. Vercel oriente d'ailleurs aujourd'hui la plupart des workloads vers le runtime Node.js.
Un client MQTT minimal sur WebSocket
Publier en QoS 0 ne demande que trois paquets : CONNECT, PUBLISH et DISCONNECT, plus la lecture du CONNACK. La structure des octets est simple — un en-tête fixe, un champ « remaining length » de longueur variable et des chaînes UTF-8 préfixées par leur longueur (voir la structure des paquets MQTT pour les détails).
const enc = new TextEncoder();
// Chaîne UTF-8 avec un préfixe de longueur big-endian sur 2 octets
const str = (s) => { const b = enc.encode(s); return [b.length >> 8, b.length & 0xff, ...b]; };
// "remaining length" MQTT de longueur variable
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 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');
}Quelques détails sont faciles à rater :
- L'en-tête de sous-protocole. La plupart des brokers n'acceptent l'upgrade que si le client propose
Sec-WebSocket-Protocol: mqtt. Sans lui, vous obtenez en général un HTTP 400 ou le socket se ferme immédiatement. - Le chemin. Le listener WebSocket de ConnectMQ se trouve sur
/mqtt; un mauvais chemin renvoie HTTP 404 (WebSocket path 404). - Des client IDs aléatoires. Des centaines d'emplacements edge peuvent exécuter votre Worker au même instant ; un ID fixe les ferait se déconnecter mutuellement.
- Des frames binaires. MQTT sur WebSockets utilise des messages binaires, d'où l'envoi de
Uint8Array.
Si vous avez besoin d'une confirmation de remise, étendez publishPacket à la QoS 1 (en-tête 0x32 plus un packet identifier de deux octets après le topic) et attendez le PUBACK (0x40) avant de fermer. Gardez des payloads petits : le calculateur de taille de paquet affiche les octets exacts sur le réseau.
Le 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() });
// Répondre tout de suite ; terminer le publish MQTT en arrière-plan
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"Stockez les identifiants comme secrets, jamais dans wrangler.toml : npx wrangler secret put MQTT_USER et npx wrangler secret put MQTT_PASS. Notez que l'URL utilise https:// — le fetch des Workers effectue l'upgrade WebSocket dès qu'il voit l'en-tête Upgrade.
ctx.waitUntil maintient le Worker en vie après l'envoi de la réponse jusqu'à la résolution de la promesse : le visiteur n'attend donc pas le handshake avec le broker. Si l'appelant doit savoir si le publish a réussi, faites plutôt await publishMqtt(...) avant de répondre et renvoyez un 502 en cas d'échec.
TCP brut avec cloudflare:sockets
Les Cloudflare Workers peuvent aussi ouvrir des connexions TCP sortantes avec connect() de cloudflare:sockets, y compris en TLS via secureTransport: "on". Les fonctions de paquets ci-dessus fonctionnent sans modification — écrivez-les dans socket.writable au lieu de ws.send et lisez le CONNACK depuis socket.readable. Vous pouvez ainsi utiliser le port 8883 au lieu des WebSockets. Les WebSockets restent toutefois le choix le plus portable, car le même code tourne sur d'autres plateformes edge et dans les navigateurs (MQTT sur WebSockets).
Les limites à prendre en compte
Les valeurs exactes varient selon la plateforme et l'offre, et évoluent avec le temps : consultez la documentation actuelle. Les catégories qui comptent pour MQTT :
- Temps CPU et durée réelle par requête. Un handshake TLS + WebSocket + MQTT coûte plusieurs allers-retours réseau ; publiez un lot par requête plutôt qu'une connexion par message.
- Aucune réutilisation de connexion sur laquelle compter. Les isolates peuvent être réutilisés, mais traitez chaque requête comme une nouvelle connexion.
- Limites de connexions sortantes et de subrequests. Chaque WebSocket y est décompté.
- Ports sortants. Certaines plateformes restreignent les ports utilisables par les requêtes sortantes. Si le 8084 n'est pas autorisé, utilisez un listener du broker sur le 443 (MQTT sur le port 443).
- Débit de connexions côté broker. Un pic de trafic sur votre site devient un pic de connexions MQTT. Pour des événements à fort volume, envisagez de les envoyer à un service d'agrégation unique qui garde une seule connexion ouverte.
Erreurs courantes
| Erreur | Cause | Correctif |
|---|---|---|
WebSocket upgrade failed: HTTP 404 | Mauvais chemin | Utiliser /mqtt |
WebSocket upgrade failed: HTTP 400 ou fermeture immédiate | Sous-protocole mqtt absent ou http:// simple vers un port TLS | Envoyer Sec-WebSocket-Protocol: mqtt, utiliser https:// avec le 8084 |
CONNACK return code 4 | Nom d'utilisateur ou mot de passe incorrect | Vérifier les secrets du Worker |
CONNACK return code 5 | Non autorisé (ACL ou compte) | Autoriser le client à se connecter et publier |
CONNACK timeout | Port bloqué ou broker injoignable | Tester d'abord l'endpoint depuis le client en ligne |
D'autres échecs propres aux WebSockets sont traités dans WebSocket connection failed.
Tester
- Ouvrez le client MQTT en ligne, connectez-vous à
broker.connectmq.comen WebSockets sécurisés et abonnez-vous àtestmqtt/edge/#. - Lancez le Worker en local avec
npx wrangler dev, puis appelez-le :curl "http://localhost:8787/checkout?device=kiosk-1". - Un message contenant le nom de l'appareil et l'horodatage doit apparaître dans le navigateur. Sinon, cherchez le message d'erreur de
publishMqttdans la console dewrangler dev. - Déployez avec
npx wrangler deployet recommencez avec l'URLworkers.dev; le champcoloindique désormais quel emplacement edge a publié le message.
Voir arriver les messages edge dans le navigateur
Les fonctions edge et MQTT se rejoignent sur le WebSocket : une cinquantaine de lignes d'encodage de paquets transforment n'importe quel runtime edge en publisher MQTT sans dépendance. Gardez ces fonctions comme des publishers éphémères, protégez leurs identifiants avec des ACL strictes et laissez les abonnements longue durée à des services capables de rester connectés.
Questions fréquentes
Puis-je utiliser MQTT.js dans un Cloudflare Worker ?
Ce n'est pas une cible prise en charge. MQTT.js attend les streams et sockets de Node.js ; avec la compatibilité Node activée, une partie peut fonctionner, mais le comportement peut changer d'une version du runtime à l'autre. Un petit encodeur MQTT sur l'API WebSocket des Workers est plus prévisible pour publier.
Une fonction edge peut-elle s'abonner à des topics MQTT ?
Seulement le temps d'une requête. Les fonctions edge sont liées à la requête et ne peuvent pas garder une connexion ouverte indéfiniment. Pour des abonnements longue durée, utilisez un service permanent ou une primitive prévue pour cela, comme les Durable Objects de Cloudflare, et vérifiez ses limites.
Quel port du broker une fonction edge doit-elle utiliser ?
Les WebSockets sécurisés (wss, port 8084 sur ConnectMQ, chemin /mqtt) sont l'option la plus portable, car tous les runtimes edge prennent en charge les WebSockets. Si votre plateforme restreint les ports sortants, utilisez un listener du broker sur le 443.


