Tutoriels

MQTT depuis des fonctions edge (Cloudflare Workers, Vercel)

Publiez des messages MQTT depuis Cloudflare Workers et des fonctions edge via WebSockets sécurisés : client sans dépendance, waitUntil, limites et tests.

Mis à jour · 9 min de lecture

MQTT depuis des fonctions edge (Cloudflare Workers, Vercel)

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

BesoinCloudflare WorkersVercel (runtime Node.js)Runtime Edge de Vercel
WebSocket sortantOui, via fetch avec Upgrade: websocketOui, via une bibliothèqueLimité — consultez la documentation actuelle
TCP / TLS sortantOui, via connect() de cloudflare:socketsOui (net / tls de Node)Non
Exécuter MQTT.js tel quelNon pris en chargeOuiNon
Garder un abonnement actifSeulement pendant une requête (Durable Objects pour plus longtemps)Seulement pendant l'exécution de la fonctionSeulement 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).

src/mqtt.js
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

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() });

    // 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');
  },
};
wrangler.toml
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

ErreurCauseCorrectif
WebSocket upgrade failed: HTTP 404Mauvais cheminUtiliser /mqtt
WebSocket upgrade failed: HTTP 400 ou fermeture immédiateSous-protocole mqtt absent ou http:// simple vers un port TLSEnvoyer Sec-WebSocket-Protocol: mqtt, utiliser https:// avec le 8084
CONNACK return code 4Nom d'utilisateur ou mot de passe incorrectVérifier les secrets du Worker
CONNACK return code 5Non autorisé (ACL ou compte)Autoriser le client à se connecter et publier
CONNACK timeoutPort bloqué ou broker injoignableTester d'abord l'endpoint depuis le client en ligne

D'autres échecs propres aux WebSockets sont traités dans WebSocket connection failed.

Tester

  1. Ouvrez le client MQTT en ligne, connectez-vous à broker.connectmq.com en WebSockets sécurisés et abonnez-vous à testmqtt/edge/#.
  2. Lancez le Worker en local avec npx wrangler dev, puis appelez-le : curl "http://localhost:8787/checkout?device=kiosk-1".
  3. Un message contenant le nom de l'appareil et l'horodatage doit apparaître dans le navigateur. Sinon, cherchez le message d'erreur de publishMqtt dans la console de wrangler dev.
  4. Déployez avec npx wrangler deploy et recommencez avec l'URL workers.dev ; le champ colo indique 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.