Back to blog

IoT Communication Protocols: MQTT, CoAP, AMQP & WebSocket

iotmqttprotocolsbackendtutorial
IoT Communication Protocols: MQTT, CoAP, AMQP & WebSocket

Welcome to Phase 2 of the IoT Patterns & Strategies Roadmap! In Phase 1, we covered the big picture — architecture layers, device types, connectivity options, and message patterns. Now it's time to go deep into the protocols that actually carry your IoT data.

Choosing the right protocol can make or break your IoT system. Pick MQTT when you should have used CoAP and your battery-powered sensors die in weeks instead of years. Pick HTTP when you should have used MQTT and your broker melts under 10,000 devices. Pick AMQP when you need lightweight pub/sub and your constrained devices can't handle the overhead.

By the end of this post, you'll know exactly which protocol to use, how to set it up, and how to bridge protocols when your system needs more than one.

What You'll Learn

✅ Set up and configure an MQTT broker (Mosquitto) from scratch
✅ Understand MQTT QoS levels and when to use each one
✅ Implement CoAP for constrained UDP-based devices
✅ Configure AMQP with RabbitMQ for enterprise IoT backends
✅ Build real-time IoT dashboards with WebSocket and MQTT-over-WebSocket
✅ Design protocol bridges for multi-protocol IoT systems
✅ Apply a decision framework to select the right protocol for any scenario


Protocol Landscape Overview

Before diving into each protocol, let's see how they relate to each other:

ProtocolTransportModelMin HeaderConnectionBest For
MQTTTCPPub/Sub2 bytesPersistentGeneral IoT telemetry
CoAPUDPRequest/Response4 bytesConnectionlessBattery-powered sensors
AMQPTCPQueues + Pub/Sub8 bytesPersistentEnterprise IoT backends
WebSocketTCPFull-duplex2 bytesPersistentBrowser dashboards
HTTPTCPRequest/Response~200+ bytesPer-requestCloud APIs, webhooks

MQTT — The IoT Standard

MQTT (Message Queuing Telemetry Transport) is the dominant IoT protocol. Designed in 1999 by Andy Stanford-Clark (IBM) and Arlen Nipper for monitoring oil pipelines over satellite links, it was built for unreliable networks with constrained bandwidth — exactly what IoT needs.

How MQTT Works

MQTT uses a publish-subscribe model with a central broker:

Key concepts:

  • Publisher: Sends messages to a topic (doesn't know who's listening)
  • Subscriber: Receives messages from topics it's interested in
  • Broker: Routes messages from publishers to subscribers
  • Topic: A hierarchical string like factory/zone-1/temp/sensor-01

Setting Up Mosquitto (MQTT Broker)

Mosquitto is the most popular open-source MQTT broker. Let's set it up with Docker:

# docker-compose.yml
services:
  mosquitto:
    image: eclipse-mosquitto:2
    container_name: mqtt-broker
    ports:
      - "1883:1883"   # MQTT
      - "9001:9001"   # WebSocket (for browser clients)
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ./mosquitto/data:/mosquitto/data
      - ./mosquitto/log:/mosquitto/log
    restart: unless-stopped
# mosquitto/config/mosquitto.conf
listener 1883
protocol mqtt
 
listener 9001
protocol websockets
 
# Authentication (disable for development only)
allow_anonymous true
 
# Persistence
persistence true
persistence_location /mosquitto/data/
 
# Logging
log_dest file /mosquitto/log/mosquitto.log
log_type all
# Start the broker
docker compose up -d
 
# Verify it's running
docker compose logs mosquitto
# Output: mosquitto version 2.x.x running

MQTT Topics and Wildcards

Topics are hierarchical strings separated by /. Design them carefully — a good topic structure is like a good database schema.

# Topic naming convention: {organization}/{location}/{device-type}/{device-id}/{data-type}
factory/building-a/sensors/temp-042/readings
factory/building-a/sensors/temp-042/status
factory/building-a/actuators/fan-01/commands
factory/building-b/sensors/humidity-003/readings

Wildcards let subscribers match multiple topics:

import mqtt from "mqtt";
 
const client = mqtt.connect("mqtt://localhost:1883");
 
client.on("connect", () => {
  // Single-level wildcard (+): matches exactly ONE level
  client.subscribe("factory/building-a/sensors/+/readings");
  // Matches: factory/building-a/sensors/temp-042/readings ✅
  // Matches: factory/building-a/sensors/humidity-003/readings ✅
  // No match: factory/building-b/sensors/temp-042/readings ❌
 
  // Multi-level wildcard (#): matches EVERYTHING below
  client.subscribe("factory/building-a/#");
  // Matches: factory/building-a/sensors/temp-042/readings ✅
  // Matches: factory/building-a/actuators/fan-01/commands ✅
  // Matches: factory/building-a/alerts ✅
  // No match: factory/building-b/anything ❌
});
 
client.on("message", (topic, message) => {
  const segments = topic.split("/");
  const building = segments[1];
  const deviceType = segments[2];
  const deviceId = segments[3];
 
  console.log(`[${building}/${deviceId}] ${message.toString()}`);
});

Topic design best practices:

RuleGoodBad
Use hierarchyfactory/zone-1/temp/s01factory_zone1_temp_s01
Don't start with /factory/sensors/.../factory/sensors/...
Keep levels meaningful{org}/{location}/{type}/{id}a/b/c/d
Avoid spacesbuilding-abuilding a
Use lowercasefactory/zone-1Factory/Zone-1

MQTT QoS Levels

QoS (Quality of Service) controls delivery guarantees. This is MQTT's killer feature — no other lightweight protocol offers this flexibility.

QoS LevelGuaranteeMessagesOverheadUse Case
QoS 0At most once (fire-and-forget)1LowestHigh-frequency GPS, ambient temperature
QoS 1At least once (may duplicate)2MediumSensor readings, status updates
QoS 2Exactly once (no loss, no dup)4HighestBilling events, actuator commands
import mqtt from "mqtt";
 
const client = mqtt.connect("mqtt://localhost:1883", {
  clientId: "sensor-node-042",
  clean: false,         // Persistent session — broker remembers subscriptions
  reconnectPeriod: 5000,
});
 
client.on("connect", () => {
  // QoS 0: GPS coordinates every second — missing one is fine
  setInterval(() => {
    client.publish("fleet/truck-07/gps", JSON.stringify({
      lat: 37.7749 + Math.random() * 0.01,
      lng: -122.4194 + Math.random() * 0.01,
      speed: 55 + Math.random() * 10,
      ts: Date.now(),
    }), { qos: 0 });
  }, 1000);
 
  // QoS 1: Temperature reading every 30s — we want all of them
  setInterval(() => {
    client.publish("factory/zone-1/temp/s042", JSON.stringify({
      value: 22.5 + Math.random() * 5,
      unit: "celsius",
      ts: Date.now(),
    }), { qos: 1 });
  }, 30000);
 
  // QoS 2: Actuator command — must execute exactly once
  client.publish("factory/zone-1/actuators/valve-01/cmd", JSON.stringify({
    action: "open",
    percentage: 75,
    requestId: crypto.randomUUID(),
  }), { qos: 2 });
});

Retained Messages and Last Will

Two MQTT features that solve common IoT problems:

Retained Messages: The broker stores the last message on a topic. New subscribers immediately receive it without waiting.

// Device publishes its status as a retained message
client.publish("devices/sensor-042/status", JSON.stringify({
  state: "online",
  firmware: "2.1.0",
  ip: "192.168.1.42",
  uptime: 86400,
}), {
  qos: 1,
  retain: true, // Broker stores this — new subscribers get it immediately
});
 
// A dashboard connecting 5 minutes later immediately gets the status
// No need to wait for the next status update

Last Will and Testament (LWT): A message the broker publishes on behalf of a device when it disconnects unexpectedly.

const client = mqtt.connect("mqtt://localhost:1883", {
  clientId: "sensor-042",
  // Last Will: published by broker if this client disconnects ungracefully
  will: {
    topic: "devices/sensor-042/status",
    payload: JSON.stringify({
      state: "offline",
      lastSeen: Date.now(),
    }),
    qos: 1,
    retain: true, // Overwrite the "online" retained message
  },
});
 
// On connect, publish "online" status
client.on("connect", () => {
  client.publish("devices/sensor-042/status", JSON.stringify({
    state: "online",
    lastSeen: Date.now(),
  }), { qos: 1, retain: true });
});
 
// If the device crashes or loses network:
// 1. TCP keepalive times out (default 60s)
// 2. Broker publishes the LWT message
// 3. Dashboard sees "offline" status
// 4. Alert service triggers notification

MQTT v3.1.1 vs v5.0

MQTT v5.0 (released 2019) adds significant features:

FeatureMQTT 3.1.1MQTT 5.0
Reason codesOnly success/failureDetailed error codes (quota exceeded, topic invalid, etc.)
Shared subscriptionsNot supported$share/group/topic — load balancing across subscribers
Message expiryNever expiresTTL per message (e.g., alert valid for 5 minutes)
Topic aliasesSend full topic each timeAssign short integer alias to long topic strings
Request/ResponseWorkaround with two topicsBuilt-in correlation data and response topic
User propertiesNot supportedKey-value metadata on messages (like HTTP headers)
Flow controlBroker controlsClient can limit in-flight messages
// MQTT v5.0 features in action
const client = mqtt.connect("mqtt://localhost:1883", {
  protocolVersion: 5,
  clientId: "sensor-042",
});
 
client.on("connect", () => {
  // Message expiry: alert expires after 5 minutes
  client.publish("alerts/high-temp", JSON.stringify({
    device: "sensor-042",
    temp: 45.2,
  }), {
    qos: 1,
    properties: {
      messageExpiryInterval: 300, // 5 minutes in seconds
    },
  });
 
  // Request/Response: ask a device for its config
  client.publish("devices/sensor-042/config/get", "", {
    qos: 1,
    properties: {
      responseTopic: "responses/sensor-042/config",
      correlationData: Buffer.from("req-001"),
    },
  });
 
  // Shared subscription: load-balance across 3 processing workers
  client.subscribe("$share/processors/telemetry/#");
  // Three workers subscribing to the same shared group
  // Each message goes to only ONE worker — automatic load balancing
});

CoAP — REST for Constrained Devices

CoAP (Constrained Application Protocol, RFC 7252) brings the familiar REST model to devices too constrained for HTTP. It runs over UDP instead of TCP, making it perfect for battery-powered sensors.

How CoAP Works

CoAP maps directly to HTTP concepts but with minimal overhead:

HTTPCoAPDifference
GET /temperatureGET /temperatureSame semantics
TCP + TLS (handshake)UDP + DTLS (lighter)No connection setup
~200+ byte headers4-byte base header50x smaller
Content-Type: application/jsonOption 12 = 50Numeric options
No built-in observeOBSERVE optionServer pushes changes

CoAP Message Types

CoAP has four message types that give you fine-grained control over reliability:

TypeNameBehaviorUse Case
CONConfirmableRetransmitted until ACK receivedCommands, important reads
NONNon-confirmableFire-and-forget, no ACKPeriodic telemetry
ACKAcknowledgmentResponse to CONConfirms receipt
RSTResetRejects messageError handling

CoAP with Node.js

import { createServer, request } from "coap";
 
// ── CoAP Server (runs on the sensor/device) ──
const server = createServer();
 
// Temperature resource
let currentTemp = 23.5;
 
server.on("request", (req, res) => {
  const path = req.url;
 
  if (path === "/temperature" && req.method === "GET") {
    res.setOption("Content-Format", "application/json");
    res.end(JSON.stringify({
      value: currentTemp,
      unit: "celsius",
      ts: Date.now(),
    }));
  }
 
  if (path === "/led" && req.method === "PUT") {
    const payload = JSON.parse(req.payload.toString());
    console.log(`LED set to: ${payload.state}`);
    res.code = "2.04"; // Changed
    res.end();
  }
 
  if (path === "/temperature" && req.method === "GET" && req.headers.Observe === 0) {
    // Observe: push updates when value changes
    res.setOption("Content-Format", "application/json");
 
    const interval = setInterval(() => {
      currentTemp += (Math.random() - 0.5) * 0.5;
      res.write(JSON.stringify({
        value: parseFloat(currentTemp.toFixed(1)),
        ts: Date.now(),
      }));
    }, 5000);
 
    res.on("finish", () => clearInterval(interval));
  }
});
 
server.listen(5683, () => {
  console.log("CoAP server listening on port 5683");
});
// ── CoAP Client (runs on the gateway or cloud) ──
import { request } from "coap";
 
// Simple GET request
const req = request({
  hostname: "sensor-042.local",
  port: 5683,
  pathname: "/temperature",
  method: "GET",
  confirmable: true, // CON message — reliable
});
 
req.on("response", (res) => {
  const data = JSON.parse(res.payload.toString());
  console.log(`Temperature: ${data.value}°C`);
});
 
req.end();
 
// PUT command to control an actuator
const cmdReq = request({
  hostname: "sensor-042.local",
  port: 5683,
  pathname: "/led",
  method: "PUT",
  confirmable: true,
});
 
cmdReq.write(JSON.stringify({ state: "on", brightness: 80 }));
cmdReq.end();
 
// Observe: subscribe to temperature changes
const obsReq = request({
  hostname: "sensor-042.local",
  port: 5683,
  pathname: "/temperature",
  method: "GET",
  observe: true,
});
 
obsReq.on("response", (res) => {
  res.on("data", (chunk: Buffer) => {
    const data = JSON.parse(chunk.toString());
    console.log(`[Observe] Temperature: ${data.value}°C`);
  });
});
 
obsReq.end();

Block-Wise Transfer

CoAP messages are limited to the UDP MTU (~1280 bytes for 6LoWPAN). For larger payloads like firmware updates, CoAP uses block-wise transfer — automatically splitting data into blocks:

// Block-wise transfer for large payloads (e.g., firmware metadata)
// The CoAP library handles block negotiation automatically
 
const req = request({
  hostname: "sensor-042.local",
  pathname: "/firmware/info",
  method: "GET",
  // Block2 option: request 256-byte blocks
});
 
req.on("response", (res) => {
  // Library reassembles blocks automatically
  const firmwareInfo = JSON.parse(res.payload.toString());
  console.log(`Firmware: ${firmwareInfo.version}, Size: ${firmwareInfo.size}`);
});
 
req.end();

When to Use CoAP vs MQTT

CriteriaCoAPMQTT
TransportUDP (connectionless)TCP (persistent connection)
NAT traversalDifficult (UDP)Easy (long-lived TCP)
Device-to-deviceNatural (direct addressing)Through broker
Resource discoveryBuilt-in (/.well-known/core)Not built-in
Observe/SubscribePer-resource observeTopic-based subscribe
Battery lifeBetter (no TCP keepalive)Good with long keepalive
MulticastSupported (UDP multicast)Not supported

Rule of thumb: Use CoAP when devices communicate directly (device-to-device or device-to-gateway in a local network). Use MQTT when devices need to reach the cloud through NATs and firewalls.


AMQP — Enterprise IoT Messaging

AMQP (Advanced Message Queuing Protocol) is heavier than MQTT but offers features that enterprise IoT backends need: message queues, routing, transactions, and guaranteed ordering.

How AMQP Works

AMQP introduces exchanges and queues between publishers and consumers:

Key AMQP concepts:

  • Exchange: Receives messages and routes them to queues based on rules
  • Queue: Stores messages until a consumer processes them
  • Binding: Rules that connect exchanges to queues (like topic filters)
  • Consumer: Processes messages from a queue, sends acknowledgment

Why AMQP for IoT?

AMQP fills a gap that MQTT doesn't cover:

FeatureMQTTAMQP
Message persistenceBroker-dependentBuilt-in durable queues
Dead letter queuesNot built-inNative support
Message routingTopic-based onlyTopic, direct, fanout, headers
Consumer acknowledgmentQoS 1/2Manual ACK with requeue
Message priorityNot supported0-255 priority levels
Transaction supportNot supportedMulti-message transactions
Flow controlLimitedPer-consumer prefetch

Setting Up RabbitMQ for IoT

# docker-compose.yml
services:
  rabbitmq:
    image: rabbitmq:3-management
    container_name: iot-rabbitmq
    ports:
      - "5672:5672"     # AMQP
      - "15672:15672"   # Management UI
      - "1883:1883"     # MQTT plugin (optional)
    environment:
      RABBITMQ_DEFAULT_USER: iot_admin
      RABBITMQ_DEFAULT_PASS: secure_password
    volumes:
      - ./rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins
      - rabbitmq_data:/var/lib/rabbitmq
    restart: unless-stopped
 
volumes:
  rabbitmq_data:
# rabbitmq/enabled_plugins
[rabbitmq_management, rabbitmq_mqtt, rabbitmq_web_stomp].

AMQP with Node.js (amqplib)

import amqplib from "amqplib";
 
// ── Producer: IoT Gateway sends telemetry ──
async function publishTelemetry() {
  const connection = await amqplib.connect("amqp://iot_admin:secure_password@localhost");
  const channel = await connection.createChannel();
 
  // Declare a topic exchange for IoT data
  const exchange = "iot.telemetry";
  await channel.assertExchange(exchange, "topic", { durable: true });
 
  // Publish sensor data with routing key
  const routingKey = "factory.zone1.temperature";
  const message = {
    deviceId: "sensor-042",
    value: 28.5,
    unit: "celsius",
    timestamp: new Date().toISOString(),
  };
 
  channel.publish(
    exchange,
    routingKey,
    Buffer.from(JSON.stringify(message)),
    {
      persistent: true,        // Survive broker restart
      contentType: "application/json",
      messageId: crypto.randomUUID(),
      timestamp: Date.now(),
      headers: {
        "x-device-type": "temperature-sensor",
        "x-firmware": "2.1.0",
      },
    }
  );
 
  console.log(`Published: ${routingKey} → ${message.value}°C`);
}
// ── Consumer: Temperature processing service ──
async function consumeTemperature() {
  const connection = await amqplib.connect("amqp://iot_admin:secure_password@localhost");
  const channel = await connection.createChannel();
 
  const exchange = "iot.telemetry";
  const queue = "temperature-processor";
 
  await channel.assertExchange(exchange, "topic", { durable: true });
  await channel.assertQueue(queue, {
    durable: true,
    deadLetterExchange: "iot.dlx", // Failed messages go here
    messageTtl: 3600000,           // 1 hour TTL
  });
 
  // Bind: only receive temperature messages from any zone
  await channel.bindQueue(queue, exchange, "factory.*.temperature");
 
  // Prefetch: process 10 messages at a time (flow control)
  await channel.prefetch(10);
 
  channel.consume(queue, (msg) => {
    if (!msg) return;
 
    try {
      const data = JSON.parse(msg.content.toString());
      console.log(`Processing: ${data.deviceId} → ${data.value}°C`);
 
      // Check for alerts
      if (data.value > 35) {
        // Publish alert to a different routing key
        channel.publish(
          exchange,
          "factory.alerts.temperature",
          Buffer.from(JSON.stringify({
            type: "highTemperature",
            device: data.deviceId,
            value: data.value,
            threshold: 35,
            timestamp: new Date().toISOString(),
          })),
          { persistent: true }
        );
      }
 
      channel.ack(msg); // Acknowledge successful processing
    } catch (error) {
      // Reject and don't requeue — send to dead letter queue
      channel.nack(msg, false, false);
    }
  });
 
  console.log("Temperature processor listening...");
}

Dead Letter Queues for IoT

Failed messages shouldn't disappear. AMQP dead letter queues catch messages that:

  • Are rejected by a consumer (nack without requeue)
  • Exceed their TTL (stale sensor data)
  • Overflow a queue (back-pressure)
async function setupDeadLetterQueue() {
  const connection = await amqplib.connect("amqp://localhost");
  const channel = await connection.createChannel();
 
  // Dead letter exchange
  await channel.assertExchange("iot.dlx", "topic", { durable: true });
  await channel.assertQueue("iot.failed-messages", { durable: true });
  await channel.bindQueue("iot.failed-messages", "iot.dlx", "#");
 
  // Main queue with dead letter routing
  await channel.assertQueue("temperature-processor", {
    durable: true,
    deadLetterExchange: "iot.dlx",
    deadLetterRoutingKey: "failed.temperature",
    messageTtl: 3600000, // Messages older than 1 hour → DLQ
    maxLength: 100000,   // Queue overflow → DLQ
  });
 
  // Monitor the dead letter queue
  channel.consume("iot.failed-messages", (msg) => {
    if (!msg) return;
    const data = JSON.parse(msg.content.toString());
    const reason = msg.properties.headers?.["x-death"]?.[0]?.reason;
    console.error(`Dead letter [${reason}]: ${JSON.stringify(data)}`);
    // Log to monitoring system, trigger investigation
    channel.ack(msg);
  });
}

WebSocket — Real-Time IoT Dashboards

WebSocket provides full-duplex communication over a single TCP connection. For IoT, it's the bridge between your backend and browser-based dashboards.

WebSocket vs MQTT-over-WebSocket

You have two options for browser-based real-time IoT:

ApproachHow It WorksProsCons
WebSocketCustom WS server pushes dataFull control, simpleMust build pub/sub yourself
MQTT-over-WSBrowser connects to MQTT broker via WSLeverage existing MQTT topicsBrowser gets all MQTT complexity

Option 1: Custom WebSocket Server

import { WebSocketServer, WebSocket } from "ws";
import mqtt from "mqtt";
 
// Bridge MQTT telemetry to WebSocket clients (dashboards)
const wss = new WebSocketServer({ port: 8080 });
const mqttClient = mqtt.connect("mqtt://localhost:1883");
 
// Track which topics each WebSocket client is watching
const clientSubscriptions = new Map<WebSocket, Set<string>>();
 
mqttClient.on("connect", () => {
  mqttClient.subscribe("factory/#");
  console.log("MQTT bridge connected");
});
 
mqttClient.on("message", (topic, payload) => {
  const message = JSON.stringify({
    topic,
    data: JSON.parse(payload.toString()),
    receivedAt: new Date().toISOString(),
  });
 
  // Forward to interested WebSocket clients
  wss.clients.forEach((ws) => {
    if (ws.readyState !== WebSocket.OPEN) return;
 
    const subs = clientSubscriptions.get(ws);
    if (!subs) return;
 
    // Check if any subscription pattern matches this topic
    for (const pattern of subs) {
      if (topicMatches(pattern, topic)) {
        ws.send(message);
        break;
      }
    }
  });
});
 
wss.on("connection", (ws) => {
  clientSubscriptions.set(ws, new Set());
 
  ws.on("message", (raw) => {
    const msg = JSON.parse(raw.toString());
 
    if (msg.type === "subscribe") {
      clientSubscriptions.get(ws)?.add(msg.topic);
      console.log(`Client subscribed to: ${msg.topic}`);
    }
 
    if (msg.type === "unsubscribe") {
      clientSubscriptions.get(ws)?.delete(msg.topic);
    }
  });
 
  ws.on("close", () => {
    clientSubscriptions.delete(ws);
  });
});
 
// Simple MQTT-style topic matching
function topicMatches(pattern: string, topic: string): boolean {
  const patternParts = pattern.split("/");
  const topicParts = topic.split("/");
 
  for (let i = 0; i < patternParts.length; i++) {
    if (patternParts[i] === "#") return true;
    if (patternParts[i] === "+") continue;
    if (patternParts[i] !== topicParts[i]) return false;
  }
 
  return patternParts.length === topicParts.length;
}

Option 2: MQTT-over-WebSocket (Browser Client)

Most MQTT brokers support WebSocket transport. Browsers connect directly to the MQTT broker:

// Browser-side: connect to MQTT broker via WebSocket
import mqtt from "mqtt";
 
// Connect to Mosquitto's WebSocket port (9001)
const client = mqtt.connect("ws://localhost:9001");
 
client.on("connect", () => {
  console.log("Connected to MQTT broker via WebSocket");
 
  // Subscribe to temperature data for a specific zone
  client.subscribe("factory/zone-1/sensors/+/readings");
});
 
client.on("message", (topic, message) => {
  const data = JSON.parse(message.toString());
  const deviceId = topic.split("/")[3];
 
  // Update dashboard UI
  updateChart(deviceId, data.value, data.timestamp);
  updateGauge(deviceId, data.value);
 
  // Check for alerts
  if (data.value > 35) {
    showAlert(`${deviceId}: ${data.value}°C exceeds threshold!`);
  }
});
 
function updateChart(deviceId: string, value: number, timestamp: number) {
  // Push data point to chart library (Chart.js, D3, etc.)
  console.log(`Chart update: ${deviceId} = ${value}°C at ${new Date(timestamp)}`);
}
 
function updateGauge(deviceId: string, value: number) {
  console.log(`Gauge update: ${deviceId} = ${value}°C`);
}
 
function showAlert(message: string) {
  console.warn(`ALERT: ${message}`);
}

Protocol Bridging

Real-world IoT systems use multiple protocols. A protocol bridge translates between them:

Building a Multi-Protocol Gateway

import mqtt from "mqtt";
import { createServer as createCoapServer } from "coap";
import amqplib from "amqplib";
 
// ── Multi-protocol IoT gateway ──
class ProtocolBridge {
  private mqttClient: mqtt.MqttClient;
  private amqpChannel: amqplib.Channel | null = null;
 
  constructor() {
    // Outbound: MQTT for real-time telemetry
    this.mqttClient = mqtt.connect("mqtt://broker.example.com", {
      clientId: "gateway-bridge-01",
    });
  }
 
  async initialize() {
    // Outbound: AMQP for reliable processing
    const amqpConn = await amqplib.connect("amqp://localhost");
    this.amqpChannel = await amqpConn.createChannel();
    await this.amqpChannel.assertExchange("iot.ingestion", "topic", {
      durable: true,
    });
 
    // Inbound: CoAP server for local sensor data
    this.startCoapListener();
 
    console.log("Protocol bridge initialized");
  }
 
  private startCoapListener() {
    const server = createCoapServer();
 
    server.on("request", (req, res) => {
      if (req.method !== "POST") {
        res.code = "4.05"; // Method Not Allowed
        res.end();
        return;
      }
 
      const data = JSON.parse(req.payload.toString());
      const deviceId = req.url.split("/")[1]; // /sensor-042/telemetry
 
      // Bridge CoAP → MQTT (real-time)
      this.mqttClient.publish(
        `devices/${deviceId}/telemetry`,
        JSON.stringify(data),
        { qos: 1 }
      );
 
      // Bridge CoAP → AMQP (reliable processing)
      this.amqpChannel?.publish(
        "iot.ingestion",
        `telemetry.${deviceId}`,
        Buffer.from(JSON.stringify({
          ...data,
          gatewayId: "gateway-01",
          bridgedFrom: "coap",
          receivedAt: new Date().toISOString(),
        })),
        { persistent: true }
      );
 
      res.code = "2.01"; // Created
      res.end();
    });
 
    server.listen(5683, () => {
      console.log("CoAP listener on port 5683");
    });
  }
}
 
const bridge = new ProtocolBridge();
bridge.initialize();

MQTT-to-HTTP Webhook Bridge

A common pattern: forward MQTT alerts to HTTP endpoints (Slack, PagerDuty, custom APIs):

import mqtt from "mqtt";
 
// Bridge MQTT alerts to HTTP webhooks
const client = mqtt.connect("mqtt://localhost:1883");
 
const webhooks: Record<string, string> = {
  "alerts/temperature": "https://hooks.slack.com/services/xxx",
  "alerts/device-offline": "https://api.pagerduty.com/events",
  "alerts/battery-low": "https://your-api.com/webhooks/battery",
};
 
client.on("connect", () => {
  // Subscribe to all alert topics
  client.subscribe("alerts/#");
});
 
client.on("message", async (topic, payload) => {
  const data = JSON.parse(payload.toString());
 
  // Find matching webhook
  for (const [pattern, url] of Object.entries(webhooks)) {
    if (topic.startsWith(pattern.replace("#", ""))) {
      try {
        await fetch(url, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            source: "iot-bridge",
            topic,
            data,
            timestamp: new Date().toISOString(),
          }),
        });
        console.log(`Forwarded ${topic} → ${url}`);
      } catch (error) {
        console.error(`Webhook failed for ${topic}: ${error}`);
      }
    }
  }
});

Protocol Selection Decision Framework

Use this decision tree to pick the right protocol:

Protocol Selection Matrix

ScenarioProtocolWhy
10,000 sensors sending temperature every 30sMQTT (QoS 0 or 1)Low overhead, persistent connections, broker handles routing
Battery-powered soil sensors, local networkCoAPUDP saves battery, no connection overhead, observe for changes
Factory floor with guaranteed delivery requirementsAMQPMessage persistence, dead letter queues, transaction support
Real-time dashboard showing live sensor dataMQTT-over-WebSocketReuse existing MQTT topics, browser-native
Firmware OTA updates (10MB+ files)HTTP/HTTPSRange requests, CDN-friendly, resume interrupted downloads
Sending alerts to Slack/PagerDutyHTTP webhooksUniversal compatibility, simple integration
Device-to-device in sensor meshCoAPDirect addressing, multicast support, no broker needed
Multi-system enterprise integrationAMQPExchanges, routing keys, message transformation
Mixed: devices + dashboard + backendMQTT + AMQP + WS bridgeMQTT for devices, AMQP for backend, WS bridge for dashboards

Hybrid Architecture Example

Most production IoT systems use multiple protocols:


Common Beginner Mistakes

Mistake 1: Using QoS 2 for Everything

// ❌ BAD: QoS 2 for high-frequency telemetry
setInterval(() => {
  client.publish("sensors/gps", JSON.stringify(gpsData), {
    qos: 2, // 4 round-trips per message × 1000 devices × 1 msg/sec = overloaded broker
  });
}, 1000);
// ✅ GOOD: Match QoS to the data's importance
// GPS every second: QoS 0 (missing one is fine)
client.publish("sensors/gps", JSON.stringify(gpsData), { qos: 0 });
 
// Temperature every 30s: QoS 1 (we want all readings)
client.publish("sensors/temp", JSON.stringify(tempData), { qos: 1 });
 
// Actuator command: QoS 2 (must execute exactly once)
client.publish("actuators/valve/cmd", JSON.stringify(cmd), { qos: 2 });

Mistake 2: No Topic Naming Convention

// ❌ BAD: Inconsistent, flat topic names
client.publish("temp_sensor_42", data);
client.publish("TemperatureSensor/42", data);
client.publish("sensor-temperature-42", data);
// Can't use wildcards effectively, no hierarchy
// ✅ GOOD: Consistent hierarchical topics
client.publish("acme/factory-1/sensors/temp-042/readings", data);
client.publish("acme/factory-1/sensors/humidity-003/readings", data);
client.publish("acme/factory-1/actuators/fan-01/commands", cmd);
// Easy to subscribe: "acme/factory-1/sensors/+/readings"
// Easy to monitor: "acme/factory-1/#"

Mistake 3: Using MQTT When CoAP Is Better

// ❌ BAD: MQTT for battery-powered device-to-gateway on local network
// MQTT requires TCP connection (3-way handshake + keepalive packets)
// On a coin battery, TCP keepalives drain power even when idle
const client = mqtt.connect("mqtt://gateway.local");
// Keepalive every 60s = ~1440 TCP packets/day even with no data
// ✅ GOOD: CoAP for local, battery-powered devices
// UDP: send one packet, no connection overhead, no keepalives
const req = request({
  hostname: "gateway.local",
  port: 5683,
  pathname: "/telemetry",
  method: "POST",
  confirmable: false, // NON: fire-and-forget (even less overhead)
});
req.write(JSON.stringify(sensorData));
req.end();
// 0 packets between readings — battery lasts months longer

Mistake 4: Not Using Retained Messages for Status

// ❌ BAD: Dashboard shows "unknown" until next status update
client.publish("devices/sensor-042/status", JSON.stringify({
  state: "online",
}));
// New dashboard subscriber sees nothing until next publish
// ✅ GOOD: Retained message — new subscribers get status immediately
client.publish("devices/sensor-042/status", JSON.stringify({
  state: "online",
  firmware: "2.1.0",
  uptime: 86400,
}), { retain: true });
// Dashboard connecting 5 minutes later immediately shows "online"

Summary and Key Takeaways

MQTT is the default IoT protocol — lightweight pub/sub with QoS levels for flexible delivery guarantees
Match QoS to data importance — QoS 0 for telemetry, QoS 1 for readings, QoS 2 for commands
CoAP is ideal for constrained devices — UDP transport, REST semantics, observe pattern, saves battery
AMQP handles enterprise requirements — durable queues, dead letter routing, transactions, priority
WebSocket bridges IoT to browsers — use MQTT-over-WS or a custom bridge for live dashboards
Design topic hierarchies carefully — consistent naming enables powerful wildcard subscriptions
Use retained messages and LWT — solve the "what's the current status?" problem elegantly
Bridge protocols when needed — most production systems use 2-3 protocols with translation layers
MQTT v5.0 adds shared subscriptions — native load balancing across consumers without application logic


What's Next

Now that you understand the protocols, it's time to explore where processing happens — at the edge, in the fog, or in the cloud.

Next: IOT-4: Edge Computing & Fog Architecture — Why edge computing matters for IoT, edge vs fog vs cloud comparison, edge runtime options (AWS Greengrass, Azure IoT Edge, K3s), and offline-first design patterns.



This is Part 3 of the IoT Patterns & Strategies series. Start from the beginning or jump to any topic that interests you.

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.