MQTT (originally MQ Telemetry Transport, sometimes expanded as Message Queuing Telemetry Transport) is a lightweight publish-subscribe messaging protocol built on top of TCP/IP. It was designed in 1999 by Andy Stanford-Clark (IBM) and Arlen Nipper for monitoring oil pipelines over unreliable satellite links -- exactly the kind of constrained environment that makes it ideal for ESP32 projects today. For a beginner-friendly walkthrough with a complete working example, see Introduction to MQTT. This page is a more technical reference.
🔗Protocol Versions
| Version | Year | Status | Notes |
|---|---|---|---|
| 3.1 | 2010 | Legacy | Rarely used today |
| 3.1.1 | 2014 | Most common in IoT | OASIS standard, supported by all brokers and libraries |
| 5.0 | 2019 | Newer | Adds shared subscriptions, reason codes, topic aliases, message expiry |
PubSubClient (the standard ESP32 library) supports MQTT 3.1.1. If you need MQTT 5.0 features, look at the arduino-mqtt library by Joel Gaehwiler or esp-mqtt (ESP-IDF).
🔗How It Works
MQTT uses a broker to mediate all communication. Devices never talk directly to each other -- they publish messages to the broker, and the broker forwards those messages to any device that has subscribed to the matching topic.
graph LR
E1[ESP32 — Temp Sensor] -->|publish<br>home/temp| B[MQTT Broker]
E2[ESP32 — Door Sensor] -->|publish<br>home/door| B
B -->|subscribe<br>home/#| D[Dashboard]
B -->|subscribe<br>home/door| P[Phone Alert]
B -->|subscribe<br>home/temp| E3[ESP32 — Thermostat]The broker handles authentication, access control, message routing, and persistence. The ESP32 only needs to maintain a single TCP connection to the broker.
🔗Topic Hierarchy
Topics are UTF-8 strings organized in a hierarchy separated by /. They are case-sensitive.
home/livingroom/temperature
home/livingroom/humidity
home/garden/soil-moisture
devices/esp32-kitchen/statusGo from general to specific (home/livingroom/temperature, not temperature/home/livingroom). Use lowercase and hyphens. Avoid leading / (the topic /home/temp has an empty first level). Topics are created on first use -- no pre-registration needed.
🔗Wildcards (Subscribe Only)
Wildcards can only be used when subscribing, never when publishing.
| Wildcard | Name | Matches | Example |
|---|---|---|---|
+ | Single level | Exactly one level | home/+/temperature matches home/livingroom/temperature and home/kitchen/temperature, but not home/floor2/kitchen/temperature |
# | Multi level | Zero or more levels; must be last character | home/# matches home/livingroom/temperature, home/door, and even home itself |
You can combine them: home/+/sensors/# matches home/livingroom/sensors/temp and home/kitchen/sensors/humidity/raw.
Warning: Subscribing to
#(alone) matches every message on the entire broker. On a public broker this can flood your ESP32. On a private broker it is useful for debugging but should not be used in production.
🔗Quality of Service (QoS)
QoS is set per message (on publish) and per subscription. The effective QoS is the lower of the two.
| QoS | Name | Delivery guarantee | Broker stores message? | Extra packets | Best for |
|---|---|---|---|---|---|
| 0 | At most once | None -- fire and forget | No | 0 | Frequent sensor readings where occasional loss is acceptable |
| 1 | At least once | Delivered, but may be duplicated | Until ACK received | 1 (PUBACK) | Important data; receiver handles duplicates |
| 2 | Exactly once | Delivered exactly once | Until handshake completes | 3 (PUBREC, PUBREL, PUBCOMP) | Payment or safety-critical commands (rare in IoT) |
On ESP32: QoS 0 and 1 are the practical choices. QoS 2 requires four packets per message and PubSubClient does not support it (it will downgrade to QoS 1). For periodic sensor readings published every few seconds, QoS 0 is fine -- if one reading is lost, the next arrives moments later.
🔗Retained Messages
When you publish with the retain flag set to true, the broker stores that message. Any new subscriber to that topic immediately receives the last retained message, even if it was published hours ago.
// Publish with retain = true (last parameter)
mqtt.publish("home/thermostat/setpoint", "22.0", true);This is useful for state information (device online/offline, current setpoint) where a new subscriber needs to know the current value without waiting for the next update.
To clear a retained message, publish an empty payload with retain set to true:
mqtt.publish("home/thermostat/setpoint", "", true);🔗Last Will and Testament (LWT)
The LWT is a message that the broker publishes on your behalf if your client disconnects unexpectedly (network failure, crash, timeout). You configure it when connecting:
// connect(clientID, username, password, willTopic, willQoS, willRetain, willMessage)
mqtt.connect("esp32-kitchen", NULL, NULL,
"devices/esp32-kitchen/status", 1, true, "offline");When the ESP32 connects successfully, you typically publish "online" to the same topic with retain. If the ESP32 crashes, the broker publishes "offline" for you. Dashboards can subscribe to devices/+/status to monitor which devices are alive.
🔗Keep-Alive
The keep-alive interval tells the broker how long to wait before considering the client dead. The client must send at least one packet (a PINGREQ if no data) within each interval.
| Parameter | Default (PubSubClient) | Typical range |
|---|---|---|
| Keep-alive | $15\,\text{s}$ | $15$--$300\,\text{s}$ |
The broker disconnects the client (and publishes the LWT) if no packet arrives within $1.5 \times$ the keep-alive interval.
mqtt.setKeepAlive(60); // Set to 60 secondsImportant: You must call
mqtt.loop()frequently in your main loop. This function sends PINGREQ packets and processes incoming messages. Ifloop()is not called within the keep-alive window, the broker will disconnect you.
🔗Clean Session
PubSubClient defaults to cleanSession = true, meaning the broker forgets all subscriptions when you disconnect. You must re-subscribe after every reconnect (which is why the example code subscribes inside reconnect()). Setting it to false makes the broker remember your subscriptions and queue QoS 1/2 messages while you are offline, but this consumes broker memory and queued messages arrive in a burst on reconnect.
🔗Network Details
| Parameter | Value |
|---|---|
| Transport | TCP/IP |
| Default port | $1883$ (unencrypted) |
| TLS port | $8883$ (encrypted) |
| WebSocket port | $8083$ or $8084$ (varies by broker) |
| Minimum packet size | $2\,\text{bytes}$ (PINGREQ) |
| Maximum packet size | $256\,\text{MB}$ (protocol limit) |
🔗ESP32 Library: PubSubClient
PubSubClient by Nick O'Leary is the standard MQTT library for Arduino and ESP32.
| Setting | Default | Notes |
|---|---|---|
| Max packet size | $256\,\text{bytes}$ | Includes topic + payload. Increase if needed. |
| Keep-alive | $15\,\text{s}$ | |
| Socket timeout | $15\,\text{s}$ | |
| MQTT version | 3.1.1 |
Increasing the packet size (needed for longer JSON payloads):
// Must be called BEFORE mqtt.connect()
mqtt.setBufferSize(512); // Increase to 512 bytesIf your publish silently fails (returns false), the payload likely exceeds the buffer size.
🔗Code Example: Connect, Publish JSON, Subscribe
#include <WiFi.h>
#include <PubSubClient.h>
const char* ssid = "YourNetwork";
const char* password = "YourPassword";
const char* mqtt_server = "test.mosquitto.org";
const char* client_id = "esp32-ref-demo";
WiFiClient net;
PubSubClient mqtt(net);
void callback(char* topic, byte* payload, unsigned int length) {
char msg[length + 1];
memcpy(msg, payload, length);
msg[length] = '\0';
Serial.printf("[%s] %s\n", topic, msg);
}
void reconnect() {
while (!mqtt.connected()) {
// Set LWT before connecting
if (mqtt.connect(client_id, NULL, NULL,
"devices/esp32-ref-demo/status", 1, true, "offline")) {
mqtt.publish("devices/esp32-ref-demo/status", "online", true);
mqtt.subscribe("home/commands/#", 1);
} else {
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
mqtt.setServer(mqtt_server, 1883);
mqtt.setBufferSize(512);
mqtt.setCallback(callback);
}
void loop() {
if (!mqtt.connected()) reconnect();
mqtt.loop();
static unsigned long last = 0;
if (millis() - last > 10000) {
last = millis();
char json[128];
snprintf(json, sizeof(json),
"{\"temp\":%.1f,\"hum\":%.1f,\"uptime\":%lu}",
23.5, 61.2, millis() / 1000);
mqtt.publish("home/livingroom/sensors", json);
}
}This sketch connects to WiFi and the MQTT broker, sets up an LWT, subscribes to a command topic with QoS 1, and publishes a JSON payload every $10\,\text{s}$.
🔗Common Brokers
| Broker | Type | Address | Notes |
|---|---|---|---|
| test.mosquitto.org | Public (testing) | test.mosquitto.org:1883 | No auth, no privacy |
| HiveMQ | Public (testing) | broker.hivemq.com:1883 | Web client for debugging |
| EMQX | Public (testing) | broker.emqx.io:1883 | Large-scale public broker |
| Mosquitto (self-hosted) | Private | Your server's IP | Lightweight, runs on Raspberry Pi |
| EMQX (self-hosted) | Private | Your server's IP | Dashboard, clustering, MQTT 5.0 |
Public brokers are for testing only. For real deployments, run your own broker with authentication and TLS (port $8883$).
🔗Common Issues
| Problem | Cause | Fix |
|---|---|---|
publish() returns false | Payload + topic exceeds buffer | Call mqtt.setBufferSize(512) or larger |
| Frequent disconnects | mqtt.loop() not called often enough | Avoid delay() in main loop; call loop() every iteration |
| Two devices fighting | Same client ID on the broker | Each device must have a unique client ID |
| Messages lost (QoS 0) | Normal -- QoS 0 has no delivery guarantee | Use QoS 1 for important messages |
| Subscriptions lost after reconnect | cleanSession = true (default) | Re-subscribe in your reconnect function |
| Cannot connect to broker | Firewall blocks port $1883$ | Check firewall rules; try WebSocket port |
| LWT not published | Client disconnected cleanly (not unexpected) | LWT only fires on ungraceful disconnect |
🔗Used In
The following pages on this site use MQTT:
- MQTT Temperature Logger -- publish DS18B20 readings to an MQTT broker
- Smart Door Sensor -- publish door open/close events
- Motion-Activated Alarm -- MQTT alerts on motion detection
- MQTT with Home Assistant -- integrate ESP32 sensors with Home Assistant via MQTT
- Custom Sensor Node -- multi-sensor node publishing to MQTT
- Wireless Sensor Network -- multiple ESP32 nodes reporting via MQTT
- Smart Thermostat -- MQTT-controlled thermostat
- LED Matrix Display -- receive display commands over MQTT