Phase 4: Connectivity 15 min read

MQTT on ESP32 — Publish Sensor Data to a Broker

Use MQTT with ESP32 to publish sensor readings and subscribe to commands. Covers PubSubClient library, broker setup, QoS levels, retained messages, and last-will topics.

Updated June 18, 2026

Why MQTT for ESP32?

When a DHT22 reads 23.4°C, you have two options: send an HTTP POST (adds ~300 bytes of headers to your 8 bytes of data) or publish an MQTT message (8 bytes, no headers). For devices publishing every 5 seconds, that bandwidth difference compounds fast. MQTT also gives you a central broker that routes messages to any number of subscribers — dashboards, databases, automation rules — without the sensor needing to know they exist.

Architecture Overview

  • Broker — the central hub (Mosquitto, HiveMQ, AWS IoT Core). Routes messages between publishers and subscribers.
  • Publisher — your ESP32, sending sensor readings to topics like home/bedroom/temperature.
  • Subscriber — a Node-RED flow, Grafana panel, or another ESP32 receiving the messages.
  • Topic — a UTF-8 string path like home/bedroom/temperature. Use / as a separator, + as a single-level wildcard, # as a multi-level wildcard.

Install PubSubClient

Open Arduino IDE → Sketch → Include Library → Manage Libraries. Search for PubSubClient (by Nick O’Leary) and install the latest version.

Connect and Publish

mqtt_publish.ino
#include <WiFi.h>
#include <PubSubClient.h>

const char* ssid        = "YourSSID";
const char* password    = "YourPassword";
const char* mqtt_server = "broker.hivemq.com";
const int   mqtt_port   = 1883;
const char* client_id   = "ESP32Sensor001";   // must be unique per broker

WiFiClient   wifiClient;
PubSubClient mqtt(wifiClient);

void connectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("Connecting to MQTT... ");
    if (mqtt.connect(client_id)) {
      Serial.println("connected");
    } else {
      Serial.printf("failed (rc=%d) — retry in 5 s\n", mqtt.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("\nWi-Fi connected");

  mqtt.setServer(mqtt_server, mqtt_port);
  mqtt.setKeepAlive(60);
  connectMQTT();
}

void loop() {
  if (!mqtt.connected()) connectMQTT();
  mqtt.loop();   // processes incoming messages and keepalives

  // Publish a fake temperature reading every 10 s
  static unsigned long last = 0;
  if (millis() - last >= 10000) {
    last = millis();
    float temp = 22.5 + (random(-10, 10) / 10.0);
    char buf[16];
    dtostrf(temp, 4, 1, buf);
    bool ok = mqtt.publish("home/bedroom/temperature", buf, true); // retained
    Serial.printf("Published: %s (%s)\n", buf, ok ? "OK" : "FAIL");
  }
}

Subscribing to Topics

mqtt_subscribe.ino
void callback(char* topic, byte* payload, unsigned int length) {
  String msg;
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  Serial.printf("[%s] %s\n", topic, msg.c_str());

  // Control LED via command topic
  if (String(topic) == "home/bedroom/led/set") {
    digitalWrite(2, msg == "ON" ? HIGH : LOW);
  }
}

// In setup():
mqtt.setCallback(callback);

// In connectMQTT(), after mqtt.connect():
mqtt.subscribe("home/bedroom/led/set");
mqtt.subscribe("home/+/command");   // wildcard — all rooms

Last Will and Testament (LWT)

LWT lets the broker announce on your behalf when your device goes offline unexpectedly:

mqtt_lwt.ino
// In setup(), before connectMQTT():
mqtt.setWill(
  "home/bedroom/status",  // topic
  "offline",              // message
  true,                   // retained
  1                       // QoS 1
);

// In connectMQTT(), after mqtt.connect():
mqtt.publish("home/bedroom/status", "online", true);

Publishing JSON with ArduinoJson

mqtt_json.ino
#include <ArduinoJson.h>

void publishSensorData(float temp, float hum) {
  JsonDocument doc;
  doc["device"]      = client_id;
  doc["temperature"] = serialized(String(temp, 1));
  doc["humidity"]    = serialized(String(hum, 1));
  doc["uptime"]      = millis() / 1000;

  char buf[256];
  serializeJson(doc, buf, sizeof(buf));
  mqtt.publish("home/bedroom/sensors", buf, true);
}

MQTT over TLS (Port 8883)

mqtt_tls.ino
#include <WiFiClientSecure.h>

const char* root_ca = R"(-----BEGIN CERTIFICATE-----
... broker root CA certificate here ...
-----END CERTIFICATE-----
)";

WiFiClientSecure secureClient;
PubSubClient     mqtt(secureClient);

// In setup():
secureClient.setCACert(root_ca);
mqtt.setServer("your-broker.com", 8883);

Reconnect Pattern

The connection can drop. Subscriptions are lost when it does. Always re-subscribe inside the reconnect function:

mqtt_reconnect.ino
void connectMQTT() {
  int retries = 0;
  while (!mqtt.connected() && retries < 5) {
    if (mqtt.connect(client_id, "username", "password")) {
      Serial.println("MQTT connected");
      // Re-subscribe every reconnect
      mqtt.subscribe("home/bedroom/led/set");
      mqtt.publish("home/bedroom/status", "online", true);
      return;
    }
    retries++;
    delay(5000);
  }
  if (!mqtt.connected()) ESP.restart(); // hard reset after 5 fails
}

Testing with MQTT Explorer

Install MQTT Explorer (free, cross-platform) to visualise topics, publish test messages, and inspect payloads while developing. Connect to your broker, subscribe to # (all topics), and watch your ESP32 messages arrive in real time.

Frequently Asked Questions

MQTT (Message Queuing Telemetry Transport) is a lightweight publish-subscribe protocol designed for low-bandwidth, unreliable networks. It is ideal for IoT because a 50-byte sensor reading costs far less bandwidth than an equivalent HTTP request.
PubSubClient by Nick O'Leary is the most widely used library for Arduino/ESP32. Install it via Library Manager (search "PubSubClient"). For more advanced features like QoS 2 and MQTT 5 support, use the MQTT library by Joel Gaehwiler.
A retained message is stored by the broker and immediately sent to any new subscriber when they first subscribe to that topic. This is useful for state topics — a new dashboard that subscribes to sensor/temperature gets the last known value right away.
QoS 0 (at most once, fire-and-forget) is fastest and uses least RAM — good for high-frequency sensor data where losing a reading is acceptable. QoS 1 (at least once) guarantees delivery but uses more RAM. QoS 2 is rarely needed on embedded devices.
An LWT is a message the broker sends automatically if the ESP32 disconnects unexpectedly (TCP timeout or power loss). Set it with client.setWill("device/status", "offline", true, 1) to let subscribers know the device went offline.
Yes. Use WiFiClientSecure with the broker's root CA certificate, then pass the secure client to PubSubClient instead of a plain WiFiClient. Use port 8883 instead of 1883.
broker.hivemq.com and test.mosquitto.org are public test brokers with no authentication required. For production, use Mosquitto on a VPS, HiveMQ Cloud free tier, or AWS IoT Core.
PubSubClient can subscribe to as many topics as memory allows, but there is a practical limit of 10–20 active subscriptions before performance degrades on the 520 KB heap. Use wildcard subscriptions (sensor/+/temperature) to reduce the count.
Check three things: (1) the keepalive interval — call client.setKeepAlive(60) and ensure your broker keepalive matches; (2) the broker might have a maximum connection time; (3) implement a reconnect loop that re-subscribes after reconnecting, since subscriptions are lost on disconnect.
Yes. Serialize your data to a JSON string using ArduinoJson and publish it: char buf[200]; serializeJson(doc, buf); client.publish("sensor/data", buf). Many brokers and dashboards like Node-RED and Grafana accept JSON payloads natively.

Projects to Build

Put this knowledge to work — try one of these hands-on projects.