What is MQTT?
MQTT (Message Queuing Telemetry Transport) is a lightweight, publish-subscribe network protocol that transports messages between devices. It was invented by Andy Stanford-Clark of IBM and Arlen Nipper of Cirrus Link in 1999 to monitor an oil pipeline over satellite connections. The protocol runs over TCP/IP and is designed for low-bandwidth, high-latency, or unreliable networks — exactly the conditions found in most IoT deployments.
At its core, MQTT follows a broker-based architecture. Clients do not communicate directly with each other. Instead, every message passes through a central server called a broker. A client that sends information is called a publisher. A client that receives information is called a subscriber. Publishers and subscribers never know about each other; they only know about topics — named logical channels that the broker uses to route messages.
Core Concepts
- Topic: A UTF-8 string that the broker uses to filter messages for each connected client. Topics support hierarchical namespacing using forward slashes, e.g.,
home/livingroom/temperature. - QoS (Quality of Service): Three levels of delivery guarantee — 0 (at most once), 1 (at least once), and 2 (exactly once).
- Retained Messages: The broker stores the last retained message on a topic and delivers it immediately to new subscribers.
- Last Will and Testament (LWT): A message that the broker publishes on a client's behalf if the client disconnects unexpectedly.
- Persistent Session: The broker remembers subscriptions, unacknowledged messages, and queued messages across reconnections.
- Keep Alive: A periodic heartbeat to keep the connection open and detect dead clients.
How the Protocol Works — The Wire Format
MQTT messages consist of a fixed header (mandatory, 2 bytes minimum), a variable header (optional), and a payload (optional). The fixed header encodes the message type, flags (including QoS level and retain flag), and remaining length. There are 14 message types in MQTT 3.1.1, including CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, PINGREQ, PINGRESP, and DISCONNECT.
Here is the structure of a PUBLISH packet's fixed header:
Byte 1: | Message Type (4 bits) | DUP | QoS (2 bits) | RETAIN |
| 0011 for PUBLISH | flag| | flag |
Byte 2+: Remaining Length (variable-length encoding, 1-4 bytes)
The variable header for PUBLISH contains the topic name (UTF-8 encoded, preceded by a 2-byte length field) and, for QoS > 0, a Packet Identifier. The payload follows as raw bytes — MQTT is completely payload-agnostic. You can send JSON, binary protobuf, plain text, or even encrypted blobs.
Why MQTT Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →MQTT has become the de facto standard for IoT communication. Here's why it dominates this space:
- Minimal Bandwidth Overhead: A single PUBLISH with QoS 0 can be as small as 2 bytes of header plus topic length and payload. Compare this to HTTP's verbose headers, cookies, and handshakes — MQTT is orders of magnitude leaner.
- Push Model, Not Polling: Subscribers receive data instantly when a publisher updates a topic. No wasteful polling cycles, which saves battery life on constrained devices.
- Asynchronous by Design: Publishers and subscribers are completely decoupled in time and space. A sensor can publish data while no subscriber is online; the broker handles buffering.
- Scalable Fan-Out: One publisher can reach thousands of subscribers through a single broker connection. This is perfect for dashboards, logging systems, and multi-device control.
- Reliable Delivery Options: QoS levels let you trade off speed versus reliability per message, not per connection.
- Bidirectional Communication: Unlike many telemetry-only protocols, MQTT allows both device-to-server and server-to-device messaging over the same persistent TCP connection.
Setting Up a Broker
The most popular open-source broker is Mosquitto, maintained by the Eclipse Foundation. You can install it on any Linux system or run it via Docker.
Installing Mosquitto on Ubuntu/Debian
sudo apt update
sudo apt install mosquitto mosquitto-clients -y
sudo systemctl enable mosquitto
sudo systemctl start mosquitto
To test locally, open two terminals. In the first, start a subscriber:
mosquitto_sub -t "test/topic" -v
In the second, publish a message:
mosquitto_pub -t "test/topic" -m "Hello, MQTT world!"
You should see test/topic Hello, MQTT world! appear in the subscriber terminal.
Running Mosquitto with Docker
docker run -d --name mosquitto \
-p 1883:1883 \
-p 9001:9001 \
-v $(pwd)/mosquitto.conf:/mosquitto/config/mosquitto.conf \
eclipse-mosquitto:latest
Port 1883 is the standard MQTT TCP port. Port 9001 is commonly used for MQTT over WebSockets (useful for browser-based clients).
Configuration File Example
A minimal mosquitto.conf with authentication:
# mosquitto.conf
listener 1883
allow_anonymous false
# Password file (create with mosquitto_passwd utility)
password_file /mosquitto/config/passwd
# Persistence for QoS 1/2 messages across restarts
persistence true
persistence_location /mosquitto/data/
# Logging
log_dest file /mosquitto/log/mosquitto.log
log_type error
log_type warning
log_type notice
Create a password file using the mosquitto_passwd utility:
mosquitto_passwd -b /path/to/passwd username password
MQTT Client Programming in Python
The paho-mqtt library is the standard Python client. Install it with:
pip install paho-mqtt
Basic Publisher
import paho.mqtt.client as mqtt
import time
import json
# Broker configuration
BROKER = "localhost"
PORT = 1883
TOPIC = "sensors/temperature"
# Create client instance
client = mqtt.Client(client_id="publisher_01")
# Set username/password if required
client.username_pw_set("user", "password")
# Connect to broker
client.connect(BROKER, PORT, keepalive=60)
# Publish a simple text message
client.publish(TOPIC, "23.5°C", qos=1)
# Publish JSON payload
payload = json.dumps({
"sensor_id": "temp_001",
"value": 23.5,
"unit": "celsius",
"timestamp": int(time.time())
})
client.publish(TOPIC, payload, qos=1, retain=False)
# Graceful disconnect
client.disconnect()
Basic Subscriber with Callbacks
import paho.mqtt.client as mqtt
import json
BROKER = "localhost"
PORT = 1883
TOPIC = "sensors/#" # Wildcard: subscribe to all sensor subtopics
def on_connect(client, userdata, flags, rc):
"""Callback for broker connection acknowledgement."""
rc_strings = {
0: "Connection successful",
1: "Connection refused - incorrect protocol version",
2: "Connection refused - invalid client identifier",
3: "Connection refused - server unavailable",
4: "Connection refused - bad username or password",
5: "Connection refused - not authorised"
}
print(f"Connection result: {rc_strings.get(rc, f'Unknown code {rc}')}")
if rc == 0:
# Subscribe to topic(s) after successful connection
client.subscribe(TOPIC, qos=1)
print(f"Subscribed to: {TOPIC}")
def on_message(client, userdata, msg):
"""Callback for incoming PUBLISH messages."""
print(f"Topic: {msg.topic}")
print(f"QoS: {msg.qos}")
print(f"Retain flag: {msg.retain}")
# Try to parse JSON, fall back to plain text
try:
data = json.loads(msg.payload.decode("utf-8"))
print(f"Parsed JSON: {data}")
except (json.JSONDecodeError, UnicodeDecodeError):
print(f"Raw payload: {msg.payload}")
def on_disconnect(client, userdata, rc):
"""Callback for disconnection."""
if rc != 0:
print(f"Unexpected disconnection. RC: {rc}")
# Create client and bind callbacks
client = mqtt.Client(client_id="subscriber_01")
client.on_connect = on_connect
client.on_message = on_message
client.on_disconnect = on_disconnect
# Optional: set credentials
client.username_pw_set("user", "password")
# Connect and start the network loop
client.connect(BROKER, PORT, keepalive=60)
# Blocking loop - processes network traffic and dispatches callbacks
# Use loop_forever() for a blocking main loop, or loop_start() for background threading
client.loop_forever()
Using loop_start() for Non-Blocking Operation
import paho.mqtt.client as mqtt
import time
import signal
import sys
client = mqtt.Client(client_id="nonblocking_client")
client.connect("localhost", 1883)
# Start network loop in background thread
client.loop_start()
# Subscribe and publish while your main thread does other work
client.subscribe("commands/#", qos=2)
for i in range(10):
client.publish("status/loop", f"Iteration {i}", qos=1)
time.sleep(1)
# Stop the background loop cleanly
client.loop_stop()
client.disconnect()
MQTT QoS Deep Dive
Understanding QoS is critical for building reliable MQTT systems. Here's what each level means in practice:
QoS 0 — At Most Once (Fire and Forget)
The message is sent once and not acknowledged. If the subscriber is offline or the network drops the packet, the message is lost. This is the fastest option and works well for high-frequency telemetry where occasional data loss is acceptable (e.g., temperature readings every second).
# QoS 0 publish — no ack expected, no retry
client.publish("sensors/noise", "42dB", qos=0)
QoS 1 — At Least Once (Acknowledged Delivery)
The sender stores the message until it receives a PUBACK from the broker. If no PUBACK arrives within a timeout, the message is retransmitted with the DUP flag set. Subscribers may receive duplicates — your application must be idempotent.
# QoS 1 publish — expects PUBACK from broker
msg_info = client.publish("alerts/warning", "High temperature!", qos=1)
msg_info.wait_for_publish() # Block until broker acknowledges
print(f"Published with mid={msg_info.mid}")
QoS 2 — Exactly Once (Four-Way Handshake)
This is the highest level and involves a four-step handshake: PUBLISH → PUBREC → PUBREL → PUBCOMP. Both sender and receiver persist state until the exchange completes. No duplicates, no loss — but higher latency and overhead. Use this for mission-critical commands like opening a valve or dispensing medication.
# QoS 2 publish — full four-way handshake
client.publish("critical/shutdown", "EMERGENCY_STOP", qos=2)
Important nuance: QoS in MQTT is per message and per hop. The QoS between publisher and broker can differ from the QoS between broker and subscriber. The broker downgrades QoS if a subscriber requested a lower QoS on its subscription. For example, if a publisher sends with QoS 2 but a subscriber subscribed with QoS 0, the broker delivers to that subscriber with QoS 0.
MQTT Topics — Design Patterns and Wildcards
Topic design is one of the most consequential decisions in an MQTT system. A well-designed topic hierarchy makes filtering, access control, and debugging straightforward.
Topic Structure Best Practices
# Good topic naming: hierarchical, specific-to-general
factory/plant_a/line_3/machine_7/temperature
factory/plant_a/line_3/machine_7/pressure
factory/plant_a/line_3/machine_7/status
# Use singular nouns, lowercase, no spaces
home/bedroom/lamp/set
home/bedroom/lamp/state
# Version your topics for backward compatibility
device/firmware/v2/update
device/firmware/v2/response
Wildcard Subscriptions
- Single-level wildcard (+): Matches exactly one topic level.
factory/+/line_3/+/temperaturematches all plants and all machines on line 3. - Multi-level wildcard (#): Matches any number of levels, but must be the last character.
factory/plant_a/#matches everything under plant_a.
# Subscribe to temperature from all machines on all lines in all plants
client.subscribe("factory/+/+/+/temperature", qos=1)
# Subscribe to everything under plant_a (including subtopics)
client.subscribe("factory/plant_a/#", qos=1)
# Note: # cannot be used mid-topic — this is INVALID:
# client.subscribe("factory/#/temperature") # WILL NOT WORK
Avoid Leading Slashes
MQTT topics do not need a leading forward slash. Using one creates an empty first level that complicates wildcard matching and is discouraged by the specification.
# Good
sensors/outdoor/humidity
# Avoid
/sensors/outdoor/humidity
Retained Messages and LWT
Retained Messages
When a publisher sets the retain flag, the broker stores the message alongside the topic. Every new subscriber to that topic immediately receives the last retained message — before any new live messages arrive. This is perfect for publishing static or slowly-changing state like device configuration, sensor metadata, or last-known values.
# Publish a retained message — new subscribers get this immediately
client.publish("device/thermostat/target_temp", "22", qos=1, retain=True)
# Clear a retained message by publishing a zero-byte payload with retain=True
client.publish("device/thermostat/target_temp", "", qos=1, retain=True)
Last Will and Testament (LWT)
LWT is a message that the broker publishes automatically if the client disconnects ungracefully (network failure, power loss, or keep-alive timeout). It's set during the CONNECT phase and cannot be changed during the session.
# Set LWT during client initialization
client = mqtt.Client(client_id="sensor_01")
# When this client disconnects unexpectedly, the broker publishes:
# "sensor_01/status" with payload "OFFLINE" at QoS 1, retained
client.will_set(
topic="sensor_01/status",
payload="OFFLINE",
qos=1,
retain=True
)
# On graceful connection, publish ONLINE so subscribers know you're up
client.connect("localhost", 1883)
client.publish("sensor_01/status", "ONLINE", qos=1, retain=True)
Persistent Sessions and Clean Start
In MQTT 5, the cleanStart flag replaces MQTT 3.1.1's cleanSession. When cleanStart=False (or clean_session=False in older clients), the broker remembers:
- All subscriptions with their QoS levels
- Undelivered QoS 1 and 2 messages
- In-flight QoS 2 exchanges
When the client reconnects, the broker replays queued messages and reinstates subscriptions automatically. This is essential for unreliable mobile or satellite links.
# Python paho-mqtt persistent session example
client = mqtt.Client(
client_id="persistent_sensor_01",
clean_session=False # Broker remembers subscriptions and queues
)
# Connect and subscribe
client.connect("localhost", 1883)
client.subscribe("commands/#", qos=2)
# Even if the client disconnects and reconnects later,
# QoS 2 messages sent to commands/# while offline will be delivered
client.loop_forever()
MQTT 5 — What's New
MQTT 5, released in 2019, adds significant improvements while maintaining backward compatibility with MQTT 3.1.1. Key additions include:
- Reason Codes: Every ACK packet now includes a detailed reason code explaining success or failure.
- Session Expiry: You can specify exactly how long the broker should retain session data after disconnection (from seconds to forever).
- Message Expiry: Individual PUBLISH messages can have a TTL; expired messages are dropped by the broker.
- User Properties: Custom key-value pairs in the variable header for application-specific metadata.
- Flow Control: Clients and brokers can negotiate receive maximums to prevent overload.
- Payload Format Indicator: Explicitly mark payloads as UTF-8 or arbitrary bytes.
- Topic Alias: Map frequently-used topic strings to small integers to save bandwidth.
# MQTT 5 example with paho-mqtt v2
import paho.mqtt.client as mqtt
from paho.mqtt.properties import Properties
from paho.mqtt.packettypes import PacketTypes
client = mqtt.Client(
client_id="mqtt5_client",
protocol=mqtt.MQTTv5
)
# Set session expiry to 3600 seconds (1 hour)
properties = Properties(PacketTypes.CONNECT)
properties.SessionExpiryInterval = 3600
client.connect("localhost", 1883, clean_start=mqtt.MQTT_CLEAN_START_FIRST)
# Publish with message expiry and user properties
pub_props = Properties(PacketTypes.PUBLISH)
pub_props.MessageExpiryInterval = 300 # Expire after 5 minutes if not delivered
pub_props.UserProperty = [("source", "backend"), ("priority", "high")]
client.publish(
"events/alerts",
"Server restart required",
qos=2,
properties=pub_props
)
client.loop_forever()
Security Considerations
A production MQTT deployment requires multiple layers of security:
TLS/SSL Encryption
Always use TLS in production. Plain-text MQTT over port 1883 exposes credentials and payloads to anyone on the network path.
# TLS-secured MQTT client
import ssl
client = mqtt.Client(client_id="secure_client")
client.tls_set(
ca_certs="/etc/mosquitto/certs/ca.crt", # CA certificate
certfile="/etc/mosquitto/certs/client.crt", # Client certificate
keyfile="/etc/mosquitto/certs/client.key", # Client private key
cert_reqs=ssl.CERT_REQUIRED, # Require broker certificate verification
tls_version=ssl.PROTOCOL_TLSv1_2
)
client.username_pw_set("user", "password")
client.connect("mqtt.example.com", 8883) # Port 8883 is standard MQTT+TLS
client.loop_forever()
Broker-Side ACLs
Mosquitto supports access control lists to restrict which clients can publish or subscribe to which topics. Create an ACL file:
# /etc/mosquitto/acl.conf
# User 'sensor_temp' can only publish to its own sensor topic
user sensor_temp
topic write sensors/temperature/#
topic read sensors/temperature/#
# User 'dashboard' can only read all sensor data
user dashboard
topic read sensors/#
topic deny write #
# Admin has full access
user admin
topic readwrite #
Reference this ACL file in mosquitto.conf:
acl_file /etc/mosquitto/acl.conf
Additional Security Practices
- Never hardcode credentials: Use environment variables, secrets managers, or secure elements on devices.
- Rotate client certificates: Set short expiry times and automate renewal.
- Rate limiting: Configure broker throttling to prevent a single misbehaving client from saturating the system.
- Isolate networks: Place brokers in protected VLANs; use firewalls to restrict which IPs can connect.
- Audit logging: Log all connection attempts, topic subscriptions, and publishes for forensic analysis.
Testing and Debugging MQTT
mosquitto_sub with Verbose Output
# Monitor all traffic on the broker (requires wildcard subscription access)
mosquitto_sub -t "#" -v -d
# -v shows topic alongside payload
# -d enables debug output showing message IDs, QoS, and retain flags
MQTT Explorer (GUI Tool)
MQTT Explorer is an excellent graphical client for visualizing topic trees, retained messages, and real-time traffic. It runs on Windows, macOS, and Linux. Connect it to your broker and browse the entire topic namespace interactively.
Python Debug Client
import paho.mqtt.client as mqtt
def on_message(client, userdata, msg):
print(f"\n--- Message Received ---")
print(f"Topic: {msg.topic}")
print(f"Payload: {msg.payload}")
print(f"QoS: {msg.qos}")
print(f"Retained: {msg.retain}")
print(f"Mid: {msg.mid}")
print(f"Timestamp: {msg.timestamp}")
client = mqtt.Client(client_id="debug_listener")
client.on_message = on_message
# Enable full debug logging
import logging
logging.basicConfig(level=logging.DEBUG)
client.connect("localhost", 1883)
client.subscribe("#", qos=2)
client.loop_forever()
Common Pitfalls and How to Avoid Them
- Topic explosion: Avoid overly granular topics like
device/123/sensor/456/reading/789. Group related data and use JSON payloads when fine-grained filtering isn't needed. - Subscribing to # on a busy broker: A global wildcard subscription receives every single message. On a broker handling thousands of messages per second, this can overwhelm your client. Use the most specific wildcard possible.
- Forgetting the retain flag: A retained message with stale data can mislead new subscribers. Always plan a strategy to clear or update retained messages.
- Mixing QoS expectations: If a publisher uses QoS 0 and the subscriber expects reliable delivery, messages will be lost. Align QoS end-to-end.
- Not handling reconnects: Network interruptions are normal in IoT. Always implement reconnect logic. Paho-mqtt's
loop_forever()handles automatic reconnection by default, but custom applications usingloop()must callreconnect()explicitly. - Client ID collisions: Two clients connecting with the same client ID will kick each other off (the broker disconnects the older one). Use unique, meaningful client IDs per device.
- Blocking the callback thread: The
on_messagecallback runs in the network thread. Long-running processing in that callback blocks incoming message handling. Offload heavy work to a queue or thread pool.
# Example: Offloading message processing to avoid blocking
import threading
import queue
work_queue = queue.Queue()
def on_message(client, userdata, msg):
# Immediately enqueue; callback returns quickly
work_queue.put((msg.topic, msg.payload))
def worker():
while True:
topic, payload = work_queue.get()
# Perform time-consuming processing here
process_complex_data(topic, payload)
work_queue.task_done()
# Start worker threads
for _ in range(4):
threading.Thread(target=worker, daemon=True).start()
# Now connect and subscribe — on_message stays fast
client = mqtt.Client()
client.on_message = on_message
client.connect("localhost", 1883)
client.subscribe("#", qos=1)
client.loop_forever()
Scalability Patterns
Bridging Brokers
Mosquitto supports broker bridging — connecting two MQTT brokers so that messages published on one are forwarded to the other. This enables geographic distribution and high availability.
# Bridge configuration in mosquitto.conf
connection cloud-bridge
address mqtt.cloud-provider.com:8883
remote_username bridge_user
remote_password bridge_password
topic sensors/# both 1
bridge_cafile /etc/mosquitto/certs/ca.crt
bridge_insecure false
try_private true
start_type automatic
cleansession false
Horizontal Scaling with EMQX or HiveMQ
For truly massive deployments (millions of concurrent connections), consider clustered brokers like EMQX or HiveMQ. These use distributed erlang-based or Kubernetes-native architectures to share topic trees and session state across nodes while presenting a single logical broker to clients.
Load Balancing
Use TCP load balancers (HAProxy, NGINX Stream module) in front of multiple broker instances. Configure sticky sessions based on client ID to ensure persistent session state remains on the same broker node.
# Example NGINX stream configuration for MQTT load balancing
stream {
upstream mqtt_backend {
hash $remote_addr consistent;
server broker1:1883;
server broker2:1883;
server broker3:1883;
}
server {
listen 1883;
proxy_pass mqtt_backend;
proxy_timeout 300s;
}
}
MQTT in Web Browsers
Browsers cannot open raw TCP sockets, but MQTT over WebSocket (typically on port 9001) bridges this gap. The JavaScript library mqtt.js is the standard choice.
<!-- Browser-based MQTT client using mqtt.js -->
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
<script>
// Connect via WebSocket
const client = mqtt.connect('ws://localhost:9001', {
clientId: 'web_client_' + Math.random().toString(16).substr(2, 8),
username: 'user',
password: 'password'
});
client.on('connect', () => {
console.log('Connected to broker');
client.subscribe('sensors/#', { qos: 1 });
});
client.on('message', (topic, message) => {
console.log(`Topic: ${topic}`);
console.log(`Payload: ${message.toString()}`);
});
// Publish from browser
document.getElementById('sendBtn').addEventListener('click', () => {
client.publish('commands/led', 'TOGGLE', { qos: 1 });
});
</script>
Best Practices Summary
- Use QoS 1 as default: It provides reliable delivery without the overhead of QoS 2. Reserve QoS 2 for truly critical operations where duplicates are unacceptable.
- Design topics as a tree: Structure topics from general to specific. This makes wildcard subscriptions intuitive and ACL rules straightforward.
- Keep payloads small: MQTT excels with small messages. If you need to send large data, consider chunking or offloading to HTTP/object storage and using MQTT for signaling.
- Always set LWT: This is one of MQTT's most valuable features. Use it to track device online/offline status reliably.
- Use persistent sessions for mobile devices: If a device has intermittent connectivity, persistent sessions ensure no data is lost during disconnections.
- Encrypt everything: Use TLS even on private networks. The overhead is minimal, and the protection is essential.
- Monitor broker health: Track connection counts, message rates, and storage usage. Set alerts for anomalies.
- Version your topic schemas: Include a version identifier in topic paths or payloads so subscribers can handle schema evolution gracefully.
- Test with real network conditions: Simulate packet loss, latency, and disconnections in your test environment. MQTT handles these gracefully, but your application logic must too.
Conclusion
MQTT has earned its place as the backbone of IoT communication through a combination of simplicity, efficiency, and reliability. Its publish-subscribe model decouples producers and consumers, its minimal wire overhead conserves bandwidth and battery life, and its QoS system provides fine-grained control over message delivery guarantees. From a single Raspberry Pi monitoring a greenhouse to fleets of millions of connected vehicles, MQTT scales across the entire spectrum of connected systems.
The protocol's evolution from MQTT 3.1.1 to MQTT 5 brings modern features like session expiry, flow control, and user properties while preserving the lightweight spirit that made it successful. By following the topic design principles, security practices, and code patterns outlined in this guide, you can build MQTT systems that are robust, maintainable, and ready for production. Whether you're publishing sensor data from a microcontroller, building a real-time dashboard in a browser, or orchestrating commands across a distributed industrial system, MQTT provides the reliable messaging foundation you need.