MQTT

MQTT is a lightweight publish/subscribe messaging protocol designed for small devices and unreliable networks. A temperature sensor, an ESP32 board or a smart bulb can send a few bytes to a central server, and any number of apps can receive them instantly. This lesson explains the moving parts: the broker, topics, the three delivery levels (QoS), the special features such as retained messages and Last Will, and what a packet looks like on the wire.

What Is MQTT?

MQTT (originally short for MQ Telemetry Transport) runs on top of TCP, so delivery of the bytes is reliable and ordered. Its message header can be as small as 2 bytes, which keeps battery and bandwidth use low. You will find it in sensor networks, smart-home systems, vehicle and factory telemetry and mobile notifications. The two versions in everyday use are 3.1.1 and 5.0. Version 5.0 keeps the same ideas and adds features such as reason codes, user properties, message expiry and shared subscriptions.

The Publish/Subscribe Model

In MQTT nobody talks to anybody directly. Every client connects to one central server called the broker. A client that has something to say publishes a message to a named topic. A client that wants to hear about that subject subscribes to the topic, and the broker forwards each matching message to it.

MQTT publishers, broker and subscribers Three publishers, a temperature sensor, an ESP32 board and a smart bulb, send PUBLISH messages to a central MQTT broker. Two subscribers, a phone app and a dashboard, send SUBSCRIBE requests to the broker and receive PUBLISH messages from it. Publishers and subscribers never connect to each other. Publishers Broker Subscribers Temperature sensor home/kitchen/temperature ESP32 board home/kitchen/humidity Smart bulb home/livingroom/light PUBLISH PUBLISH PUBLISH MQTT broker e.g. Mosquitto matches topics to subscriptions can store retained messages and sessions Phone app home/# Dashboard home/+/temperature SUBSCRIBE PUBLISH SUBSCRIBE PUBLISH Publishers and subscribers never talk to each other directly - only to the broker. They do not need to know each other, or to be online at the same moment.
A publisher sends to a topic, not to a person. The broker decides who receives it. The dashed arrows are subscriptions, the solid arrows are messages.

Compare this with HTTP, where a client sends a request and waits for one response from one server. With MQTT the sensor does not know whether zero, one or fifty apps are listening, and a subscriber receives new data the moment it is published without polling. Because both sides only talk to the broker, they can also go offline and come back independently (see persistent sessions below).

Topics and Wildcards

A topic is a text string that works like a path, with levels separated by /, for example home/livingroom/temperature. Topics are case sensitive (Home and home are different) and you do not create them in advance: publishing to a new topic is enough.

A subscription may use two wildcards. Wildcards are allowed only in subscriptions, never in the topic you publish to.

WildcardMeaningExample
+Exactly one levelhome/+/temperature
#Everything below this point (any number of levels). It must be the last character of the subscriptionhome/#
A topic tree and which subscriptions match which topics A tree with home and office at the top. Below home are livingroom and kitchen. Livingroom has temperature and light, kitchen has temperature and humidity. Office has desk, which has temperature. The subscription home/+/temperature matches home/livingroom/temperature and home/kitchen/temperature. The subscription home/# matches all four topics under home but not office/desk/temperature. Topic tree subscribe to home/+/temperature subscribe to home/# home office livingroom kitchen desk temperature light temperature humidity temperature match match no match match match match no match match no match no match + matches exactly one level: home/+/temperature matches the temperature topic of any room. # matches everything below that point and must be the last character: home/# Topics that start with $ (for example $SYS) are reserved for the broker.
Each leaf is one topic; its full name is the path from the left joined with /, for example home/kitchen/humidity.

Topics that start with $, such as $SYS/..., are reserved for the broker, which typically uses them to publish its own statistics. A subscription that starts with a wildcard, such as #, typically does not receive them. Good topic design goes from general to specific (building/floor/room/sensor), uses no leading /, and puts one kind of value in each topic.

Quality of Service (QoS)

Networks drop packets and connections break, so MQTT lets you choose how hard it tries to deliver each message. This setting is called QoS and exists for every publish and every subscription.

Message flow for MQTT QoS 0, 1 and 2 Three sequence diagrams side by side. QoS 0: the sender sends one PUBLISH and gets no reply. QoS 1: PUBLISH followed by PUBACK. QoS 2: PUBLISH, PUBREC, PUBREL, PUBCOMP. QoS 0 - at most once Sender Receiver PUBLISH 1 packet No acknowledgement. A message can be lost. QoS 1 - at least once Sender Receiver PUBLISH PUBACK 2 packets Resent if PUBACK is missing, so duplicates are possible. QoS 2 - exactly once Sender Receiver PUBLISH PUBREC PUBREL PUBCOMP 4 packets Stored until PUBCOMP arrives, so it is delivered only once. Each hop (publisher to broker, broker to subscriber) has its own QoS. A subscriber gets the lower of the publisher's QoS and its own subscription QoS.
More packets mean a stronger guarantee but more traffic and delay. Green arrows are acknowledgements.
QoSGuaranteeUse it for
0At most onceFrequent readings where the next value replaces a lost one, such as a temperature sent every few seconds
1At least onceCommands and events that must arrive and can safely be handled twice, such as "turn the light on"
2Exactly onceMessages where a duplicate would do harm, such as a billing event. It is the slowest level

Retained Messages, Last Will and Sessions

FeatureWhat it does
Retained messageIf a publisher sets the retain flag, the broker keeps the last message of that topic. Every new subscriber receives it immediately instead of waiting for the next update. Publishing an empty retained message to the topic clears it. Ideal for status values such as "online" or the last temperature
Last Will and TestamentWhen connecting, a client can give the broker a "will": a topic and a message. If the client disappears without a clean disconnect (power loss, dead Wi-Fi, keep-alive timeout), the broker publishes the will for it. Combined with a retained "online" message this gives you a reliable "device offline" signal
Persistent sessionThe broker can remember a client's subscriptions and queue QoS 1 and 2 messages while it is offline, then deliver them when it reconnects with the same client id. In 3.1.1 this is controlled by the clean session flag (set it to false to keep the session). In 5.0 it is split into clean start and a session expiry interval
Keep aliveThe client promises to send something within a chosen interval. If it has nothing to publish it sends a tiny PINGREQ and the broker answers with PINGRESP. If the broker hears nothing for roughly one and a half times the interval, it treats the client as gone (and publishes its will)

What a Packet Looks Like

Every MQTT message is a control packet. It starts with a fixed header: one byte whose upper four bits are the packet type and lower four bits are flags, followed by the Remaining Length, which says how many bytes come after it. Then, depending on the packet type, come a variable header and a payload.

Layout of an MQTT control packet A packet is the fixed header followed by an optional variable header and payload. The fixed header is one byte holding a 4 bit packet type and 4 bit flags, then the Remaining Length of 1 to 4 bytes. An example shows the value 321 encoded as the two bytes 0xC1 and 0x02, where the top bit of each byte says whether another byte follows. Fixed header: 2 to 5 bytes Depends on the packet type Packet type 4 bits Flags 4 bits Remaining Length 1 to 4 bytes Variable header e.g. topic name Payload the message data Byte 1 = packet type + flags. For PUBLISH the flags hold DUP, QoS (2 bits) and RETAIN. Remaining Length: 7 bits of value per byte, example value 321 Byte 1 (0xC1) Byte 2 (0x02) 1 1000001 = 65 0 0000010 = 2 top bit 1: another byte follows top bit 0: this is the last byte Value = 65 + 2 x 128 = 321 bytes after this field Smallest packet: 2 bytes. PINGREQ is just 0xC0 0x00 (type 12, flags 0, length 0).
Because Remaining Length uses 1 to 4 bytes, a packet can carry up to about 268 million bytes after the fixed header, but small packets pay only one length byte.
ValuePacketSent byPurpose
1CONNECTClientOpens the session: client id, keep alive, credentials, optional will
2CONNACKBrokerAccepts or refuses the connection
3PUBLISHEither sideCarries a message for a topic
4PUBACKEither sideAcknowledges a QoS 1 PUBLISH
5PUBRECEither sideQoS 2 step 1: PUBLISH received
6PUBRELEither sideQoS 2 step 2: release the message
7PUBCOMPEither sideQoS 2 step 3: flow complete
8SUBSCRIBEClientAsks for one or more topic filters
9SUBACKBrokerConfirms the subscription and the granted QoS
10UNSUBSCRIBEClientRemoves a subscription
11UNSUBACKBrokerConfirms the removal
12PINGREQClientKeep-alive request
13PINGRESPBrokerKeep-alive answer
14DISCONNECTClientClean goodbye (in 5.0 the broker can send it too)

MQTT 5.0 adds one more packet, AUTH (15), for extended authentication. Brokers listen on TCP port 1883 for plain MQTT and 8883 for MQTT over TLS.

Try It Yourself with Mosquitto

Mosquitto is a free, open-source broker that also ships the command-line clients mosquitto_sub and mosquitto_pub. Install it on your own computer, then open two terminals. In the first one, subscribe to the temperature of every room (-v prints the topic with each message):

mosquitto_sub -h localhost -t "home/+/temperature" -v

In the second one, publish a value:

mosquitto_pub -h localhost -t home/kitchen/temperature -m 24.5

The first terminal prints home/kitchen/temperature 24.5. Two useful variations:

# retained: a subscriber that joins later still gets this value at once
mosquitto_pub -h localhost -t home/kitchen/temperature -m 24.5 -r

# publish with QoS 1
mosquitto_pub -h localhost -t home/kitchen/temperature -m 24.5 -q 1

Mosquitto 2.0 and later accept connections only from the same computer until you configure a listener, which is fine for this experiment. A graphical client such as MQTT Explorer shows the whole topic tree live and is handy for debugging. Public test brokers exist on the internet, but anyone can read what you send to them, so use them only for throwaway data and never for anything private.

Note: The commands above are meant to be run on your own computer, not on this website.

MQTT on the ESP32

On an ESP32 the usual choice is the PubSubClient library (install it from the Arduino IDE Library Manager). It sits on top of a normal Wi-Fi client, so first make sure your board can join your network as shown in Connecting to Wi-Fi. The sketch below connects, subscribes to a command topic and publishes a reading every five seconds.

This page's code is a preview, not something you can run right on this website - this server runs PHP, not the ESP32's own firmware. Upload it to your own ESP32 board (with your own network details and your own broker address) to see it work.
#include <WiFi.h>
#include <PubSubClient.h>

const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqttServer = "192.168.1.50";   // your broker's IP address or host name
const int mqttPort = 1883;

WiFiClient espClient;
PubSubClient client(espClient);

unsigned long lastPublish = 0;

// Called for every message that arrives on a topic we subscribed to
void callback(char* topic, byte* payload, unsigned int length) {
  String message;
  for (unsigned int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.print(topic);
  Serial.print(": ");
  Serial.println(message);
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT...");
    if (client.connect("esp32-kitchen-01")) {      // client id must be unique
      Serial.println("connected");
      client.subscribe("home/kitchen/led");
    } else {
      Serial.print("failed, rc=");
      Serial.println(client.state());
      delay(2000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.println(WiFi.localIP());

  client.setServer(mqttServer, mqttPort);
  client.setCallback(callback);
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();                                   // keeps the connection alive

  if (millis() - lastPublish > 5000) {
    lastPublish = millis();
    client.publish("home/kitchen/temperature", "24.5");
  }
}
  • client.setServer(host, 1883) and client.setCallback(callback) say where the broker is and which function receives incoming messages.
  • client.connect("esp32-kitchen-01") opens the MQTT session with a client id. If your broker needs a login, use the version that also takes a user name and password: client.connect(id, user, pass).
  • client.loop() must run often. It receives messages, calls your callback and sends the keep-alive pings.
  • PubSubClient is simple on purpose: it typically publishes at QoS 0 and subscribes at QoS 0 or 1, and it has a limit on the size of a message that you can raise in its settings.

Security

  • Encrypt with TLS. Use port 8883 so that credentials and data are not readable on the network (the same TLS as in HTTPS). Plain port 1883 sends everything in the clear.
  • Authenticate every client with a user name and password, or with client certificates.
  • Limit what each client may do with topic access control lists (ACLs) on the broker, for example a sensor may only publish to its own topic.
  • Never expose a broker to the internet without authentication. Open brokers are found and abused quickly.

MQTT vs HTTP

MQTTHTTP
ModelPublish/subscribe through a brokerRequest and response, client to server
ConnectionOne long-lived TCP connection per clientTypically short exchanges (connections can be reused)
OverheadVery small header, as little as 2 bytesText headers, often hundreds of bytes per request
Push to clientsBuilt in: the broker delivers as soon as something is publishedNo, the client has to ask (polling) or use something like WebSocket
Delivery guaranteesThree QoS levels, offline queuing, Last WillNone built in; the application handles retries
Typical useSensors, telemetry, smart home, notificationsWeb pages, REST APIs, file downloads

They work well together: devices often speak MQTT to the broker, while a web or mobile app reads stored data over HTTP.

Common Mistakes

  • A leading slash in the topic. /home/kitchen has an empty first level and is a different topic from home/kitchen. Most designs avoid it.
  • Wildcards in a publish topic. + and # work only in subscriptions. Publishing to home/+/temperature is invalid.
  • Forgetting client.loop(). Without it an Arduino client never receives messages and the broker drops it when the keep-alive time runs out.
  • Two clients with the same client id. The broker allows one connection per id, so the new one kicks the old one off, and they can keep taking each other's place in a loop. Give every device a unique id.
  • Using QoS 2 everywhere. It costs four packets per message. Use QoS 0 for frequent readings and QoS 1 for most commands.
  • Subscribing to # in production. An app that receives every message on the broker can be overwhelmed. Subscribe only to what you need.
Recap: MQTT is a small publish/subscribe protocol over TCP. Clients connect to a broker, publish to hierarchical topics and subscribe with the wildcards + and #, so senders and receivers never meet. QoS 0, 1 and 2 trade speed for delivery guarantees, retained messages give new subscribers the latest value, Last Will reports a dead device, and persistent sessions survive disconnects. Packets start with a type-and-flags byte and a Remaining Length. Use port 8883 with TLS and authentication in real projects.
Next steps: Continue to BLE (Bluetooth Low Energy), the short-range wireless link that often connects a sensor to a phone before MQTT carries the data onward. To build more ESP32 projects, see ESP32 Common Projects.