IoT Communication Protocols
Choosing the right communication protocol affects bandwidth usage, battery life, latency, and reliability. IoT protocols are optimized for constrained devices and unreliable networks — quite different from typical web protocols.
Why Standard Web Protocols Don't Always Work for IoT
HTTP Request/Response overhead:
┌──────────────────────────────────────┐
│ GET /temperature HTTP/1.1 │ Headers alone are
│ Host: api.example.com │ 300–800 bytes per request
│ Accept: application/json │
│ Authorization: Bearer eyJhbGc... │
│ User-Agent: Python/3.11 requests/2.28│
└──────────────────────────────────────┘
MQTT Publish overhead:
┌──────────────────────────┐
│ PUBLISH header: 2 bytes │ Fixed header = 2 bytes
│ Topic: sensors/temp │ Total packet < 30 bytes
│ Payload: 24.5 │ for a temperature reading
└──────────────────────────┘For a sensor sending data every 5 minutes on a 2G connection with 50 KB/month data cap, that difference is the gap between a working system and a runaway data bill.
MQTT — Message Queuing Telemetry Transport
MQTT is the dominant IoT protocol. It uses a publish/subscribe model with a central broker.
Device (Publisher) Broker Dashboard (Subscriber)
│ │ │
│── PUBLISH ────────►│ │
│ topic: home/temp │◄── SUBSCRIBE ─────────────│
│ payload: 24.5 │ topic: home/temp │
│ │── FORWARD ────────────────►│
│ │ home/temp: 24.5 │# Python (PC/Raspberry Pi) — MQTT publisher
import paho.mqtt.client as mqtt
import json
import time
import random
BROKER = "broker.hivemq.com" # Free public test broker
PORT = 1883
TOPIC = "bigxstar/sensors/env"
client = mqtt.Client(client_id="sensor-node-01")
def on_connect(client, userdata, flags, rc):
codes = {0: "Connected", 1: "Wrong protocol", 4: "Bad credentials"}
print(f"Broker: {codes.get(rc, f'Error {rc}')}")
client.on_connect = on_connect
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()
try:
while True:
payload = json.dumps({
"device_id": "sensor-01",
"temperature": round(20 + random.uniform(-2, 5), 2),
"humidity": round(55 + random.uniform(-5, 10), 2),
"timestamp": int(time.time())
})
result = client.publish(TOPIC, payload, qos=1)
print(f"Published [{result.rc}]: {payload}")
time.sleep(10)
except KeyboardInterrupt:
client.loop_stop()
client.disconnect()# Python — MQTT subscriber (receiving data)
import paho.mqtt.client as mqtt
import json
BROKER = "broker.hivemq.com"
TOPIC = "bigxstar/sensors/#" # # wildcard: all sub-topics
def on_connect(client, userdata, flags, rc):
print("Connected to broker")
client.subscribe(TOPIC, qos=1)
def on_message(client, userdata, msg):
try:
data = json.loads(msg.payload.decode())
print(f"[{msg.topic}]")
print(f" Device: {data['device_id']}")
print(f" Temperature: {data['temperature']} °C")
print(f" Humidity: {data['humidity']} %")
print()
except (json.JSONDecodeError, KeyError) as e:
print(f"Parse error: {e}")
client = mqtt.Client(client_id="dashboard-listener")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, 1883, 60)
client.loop_forever() # Blocks, handles reconnection automaticallyMQTT QoS Levels
| QoS | Name | Guarantee | Messages | Use When |
|---|---|---|---|---|
| 0 | At most once | No guarantee — may be lost | 1 | High-frequency, loss-tolerant (sensor stream) |
| 1 | At least once | Delivered, may be duplicated | 1+ | Commands, alerts (handle duplicates) |
| 2 | Exactly once | Delivered exactly once | 2+ | Billing, critical state changes |
# QoS in practice
client.publish(topic, payload, qos=0) # Fire and forget
client.publish(topic, payload, qos=1) # At least once (most common)
client.publish(topic, payload, qos=2) # Exactly once (2× round trip)MQTT Topic Design
Topics are hierarchical strings separated by /. Good topic design makes your system scalable.
Recommended pattern:
{org}/{location}/{device_type}/{device_id}/{measurement}
Examples:
factory/building-a/temperature/sensor-42/celsius
home/bedroom/humidity/node-01/percent
fleet/truck-007/gps/location
Wildcards (subscribe only):
+ = single level: home/+/humidity (any room)
# = multi-level: home/# (everything under home)
Avoid:
/sensor/1 (leading slash wastes bytes)
data (too generic, won't scale)
home/temp/24.5 (embedding value in topic — use payload instead)HTTP/REST — Simple but Heavy
HTTP is the protocol of the web. It's great for non-real-time reporting to cloud APIs.
# MicroPython on ESP32 — HTTP POST to REST API
import urequests
import ujson
import time
API_URL = "https://api.example.com/v1/readings"
API_KEY = "your-api-key-here"
def send_reading(temperature, humidity):
headers = {
"Content-Type": "application/json",
"X-API-Key": API_KEY
}
body = ujson.dumps({
"sensor_id": "node-01",
"temperature": temperature,
"humidity": humidity,
"timestamp": time.time()
})
try:
r = urequests.post(API_URL, data=body, headers=headers)
if r.status_code == 201:
print("Sent successfully")
else:
print(f"Error: HTTP {r.status_code}")
r.close()
except Exception as e:
print(f"Request failed: {e}")
send_reading(24.5, 61)WebSockets — Real-Time Bidirectional
WebSockets maintain a persistent TCP connection for real-time, two-way communication — ideal for live dashboards.
# Python server — WebSocket IoT data relay
import asyncio
import websockets
import json
connected_clients = set()
latest_readings = {}
async def relay(websocket):
connected_clients.add(websocket)
try:
# Send current readings immediately on connect
if latest_readings:
await websocket.send(json.dumps(latest_readings))
# Keep connection alive
async for message in websocket:
data = json.loads(message)
if data.get("type") == "sensor_data":
latest_readings.update(data)
# Broadcast to all connected dashboards
if connected_clients:
await asyncio.gather(*[
c.send(json.dumps(data))
for c in connected_clients
if c != websocket
])
finally:
connected_clients.discard(websocket)
async def main():
async with websockets.serve(relay, "0.0.0.0", 8765):
print("WebSocket server running on ws://0.0.0.0:8765")
await asyncio.Future() # Run forever
asyncio.run(main())CoAP — Constrained Application Protocol
CoAP is like HTTP but built for constrained devices. It runs over UDP (not TCP) and uses 4-byte headers.
| Feature | HTTP | CoAP |
|---|---|---|
| Transport | TCP | UDP |
| Header size | 300–800 B | 4 B |
| Methods | GET/POST/PUT/DELETE | GET/POST/PUT/DELETE |
| Reliability | Built-in (TCP) | Confirmable messages |
| Multicast | No | Yes |
| Use case | Web, REST APIs | Low-power devices, M2M |
Protocol Decision Matrix
# How to choose your IoT protocol
def choose_protocol(needs):
"""
needs = dict of requirements
"""
if needs.get("real_time_dashboard"):
return "WebSockets"
if needs.get("device_very_constrained"):
if needs.get("unreliable_network"):
return "CoAP (UDP-based, handles packet loss)"
return "MQTT QoS 0 or 1"
if needs.get("simple_cloud_api"):
return "HTTP/REST"
if needs.get("fan_out"): # One message to many devices
return "MQTT (pub/sub naturally handles this)"
if needs.get("enterprise_reliability"):
return "AMQP (RabbitMQ)"
# Default: MQTT is almost always a good choice
return "MQTT"
print(choose_protocol({"fan_out": True, "device_very_constrained": True}))
# MQTTProtocol Comparison Summary
| Protocol | Transport | Overhead | QoS | Real-time | Best For |
|---|---|---|---|---|---|
| MQTT | TCP | Very low | 0/1/2 | Near | Sensor telemetry, commands |
| HTTP | TCP | High | None | No | Cloud API reporting |
| CoAP | UDP | Very low | Confirmable | Near | Ultra-constrained devices |
| WebSockets | TCP | Low | None | Yes | Live dashboards, bidirectional |
| AMQP | TCP | Medium | Full | Near | Enterprise, financial IoT |
Summary
- MQTT is the de facto standard for IoT — lightweight, broker-based, pub/sub model
- QoS 1 is the sweet spot for most sensor data: guaranteed delivery with low overhead
- HTTP is fine for non-real-time, low-frequency cloud API calls
- WebSockets are the right choice when you need a live dashboard with instant updates
- CoAP is MQTT's sibling for devices where even TCP is too heavy
- Design MQTT topics hierarchically — avoid flat structures that won't scale
Next, we'll dive deep into wireless technologies — WiFi, BLE, Zigbee, LoRa, and cellular.