Tutorials

ESP32 MQTT Tutorial with Arduino (PubSubClient)

ESP32 MQTT tutorial with Arduino and PubSubClient: connect to WiFi and a broker, publish sensor JSON, subscribe to commands, reconnect safely and enable TLS.

Updated · 8 min read

The ESP32 is the go-to microcontroller for MQTT projects: built-in WiFi, plenty of RAM for TLS, and first-class Arduino support. This tutorial builds a complete sketch with the PubSubClient library that publishes sensor readings as JSON, listens on a command topic, reconnects on its own and — in the last step — switches to TLS.

What you need

  • An ESP32 dev board and a USB cable.
  • Arduino IDE 2.x (or PlatformIO) with the esp32 by Espressif Systems board package installed via the Boards Manager.
  • The PubSubClient library by Nick O'Leary, installed via the Library Manager.
  • A broker. We use the public HiveMQ broker on port 1883 for testing — fine for a demo, but anyone can read the topics.

Topic layout

A clean topic structure makes devices easy to manage. Each board gets its own prefix based on a unique device ID:

TopicDirectionPurpose
testmqtt/esp32/<id>/telemetryESP32 to brokerSensor JSON every 10 seconds
testmqtt/esp32/<id>/statusESP32 to brokeronline / offline (retained, Last Will)
testmqtt/esp32/<id>/cmdBroker to ESP32Commands such as led:on

See MQTT topics and wildcards for naming conventions.

The complete sketch

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

const char* WIFI_SSID     = "your-ssid";
const char* WIFI_PASSWORD = "your-wifi-password";

const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = nullptr;   // set for brokers that require auth
const char* MQTT_PASS = nullptr;

const int LED_PIN = 2;             // on-board LED on many dev boards

WiFiClient net;
PubSubClient mqtt(net);

char deviceId[24];
char topicTelemetry[64];
char topicStatus[64];
char topicCmd[64];

unsigned long lastReconnectAttempt = 0;
unsigned long lastPublish = 0;

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

  if (msg == "led:on")  digitalWrite(LED_PIN, HIGH);
  if (msg == "led:off") digitalWrite(LED_PIN, LOW);
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nWiFi connected, IP %s\n", WiFi.localIP().toString().c_str());
}

bool connectMqtt() {
  // Last Will: broker publishes "offline" (retained) if we drop unexpectedly
  bool ok = mqtt.connect(deviceId, MQTT_USER, MQTT_PASS,
                         topicStatus, 1, true, "offline");
  if (!ok) {
    Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
    return false;
  }
  Serial.println("MQTT connected");
  mqtt.publish(topicStatus, "online", true);
  mqtt.subscribe(topicCmd, 1);
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  // Unique client ID from the chip's MAC address
  uint64_t mac = ESP.getEfuseMac();
  snprintf(deviceId, sizeof(deviceId), "esp32-%04X%08X",
           (uint16_t)(mac >> 32), (uint32_t)mac);
  snprintf(topicTelemetry, sizeof(topicTelemetry), "testmqtt/esp32/%s/telemetry", deviceId);
  snprintf(topicStatus, sizeof(topicStatus), "testmqtt/esp32/%s/status", deviceId);
  snprintf(topicCmd, sizeof(topicCmd), "testmqtt/esp32/%s/cmd", deviceId);
  Serial.printf("Device ID: %s\n", deviceId);

  connectWiFi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);   // default is 256 bytes for the whole packet
  mqtt.setKeepAlive(30);
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  if (!mqtt.connected()) {
    // Non-blocking reconnect: try every 5 seconds without stalling loop()
    unsigned long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      if (connectMqtt()) lastReconnectAttempt = 0;
    }
    return;
  }

  mqtt.loop();   // must run often: handles keep-alive and incoming messages

  unsigned long now = millis();
  if (now - lastPublish > 10000) {
    lastPublish = now;
    float temperature = temperatureRead();   // ESP32 internal sensor (rough)
    char payload[128];
    snprintf(payload, sizeof(payload),
             "{\"device\":\"%s\",\"temp\":%.1f,\"rssi\":%d,\"uptime\":%lu}",
             deviceId, temperature, WiFi.RSSI(), now / 1000);
    bool sent = mqtt.publish(topicTelemetry, payload);
    Serial.printf("Publish %s: %s\n", sent ? "ok" : "FAILED", payload);
  }
}

Open the Serial Monitor at 115200 baud to see the device ID, then watch the telemetry arrive in the browser by subscribing to testmqtt/esp32/#. Publish led:on to the cmd topic and the LED lights up.

Watch your ESP32 telemetry live

How the sketch works

Unique client IDs

MQTT brokers allow only one connection per client ID. If two boards flash the same hard-coded ID such as ESP32Client, they disconnect each other in an endless loop. Deriving the ID from the eFuse MAC address keeps it unique and stable across reboots. The client ID generator explains the rules if you want a different scheme.

A non-blocking reconnect loop

Many examples use a while (!client.connected()) loop with delay(5000). That blocks everything else on the board. The millis() pattern above retries every five seconds while the rest of loop() keeps running. mqtt.state() tells you why a connect failed: -2 is a network failure, 4 bad credentials and 5 not authorized. Compare with the full list in our MQTT connection errors guide.

Buffer size and MQTT_MAX_PACKET_SIZE

PubSubClient's buffer must hold the entire packet: fixed header, topic, and payload. The default is 256 bytes, and a JSON payload with a long topic exceeds it quickly — publish() then silently returns false. Older tutorials tell you to edit MQTT_MAX_PACKET_SIZE in the library header; since version 2.8 just call setBufferSize() before connecting. The MQTT packet size calculator tells you how big your packets really are.

PubSubClient limitations

  • MQTT 3.1.1 only — no MQTT 5 properties.
  • Publishes with QoS 0 only; subscriptions can use QoS 0 or 1.
  • Retained publishes and Last Will are supported, as shown above.

Online status with Last Will and retained messages

The connect() call registers a Last Will: if the board loses power or WiFi without a clean disconnect, the broker publishes offline to the status topic after the keep-alive timeout. Right after connecting, the sketch publishes online to the same topic. Both are retained, so a dashboard that subscribes later still sees the current state immediately. This pattern is explained in Last Will and Testament and retained messages.

Testing from the command line

You do not need a second device to test the command topic. With the Mosquitto clients installed, send a command and watch telemetry (replace the ID with the one printed on the Serial Monitor):

bash
# watch everything your board publishes
mosquitto_sub -h broker.hivemq.com -t 'testmqtt/esp32/#' -v

# switch the LED on
mosquitto_pub -h broker.hivemq.com -t 'testmqtt/esp32/esp32-ABCD12345678/cmd' -m 'led:on'

More command-line recipes are in mosquitto_pub and mosquitto_sub examples.

Building JSON with ArduinoJson

snprintf is fine for a handful of fields, but for nested data or parsing JSON commands, the ArduinoJson library (version 7) is safer:

cpp
#include <ArduinoJson.h>

void publishTelemetry() {
  JsonDocument doc;
  doc["device"] = deviceId;
  doc["temp"] = temperatureRead();
  doc["rssi"] = WiFi.RSSI();

  char payload[256];
  size_t n = serializeJson(doc, payload, sizeof(payload));
  mqtt.publish(topicTelemetry, (const uint8_t*)payload, n, false);
}

Keep payloads small and flat: short keys and numbers instead of strings save RAM, airtime and broker bandwidth.

Troubleshooting state codes

mqtt.state()MeaningTypical fix
-4Connection timeoutCheck the keep-alive and that loop() runs often
-2Network connect failedWrong host or port, firewall, or TLS handshake failure
2Client ID rejectedUse a shorter or unique client ID
4Bad username or passwordCheck credentials, including case
5Not authorizedThe broker's access rules deny this client

Adding TLS with WiFiClientSecure

Plain port 1883 sends your credentials and data in clear text. Switching to TLS on port 8883 takes three changes: include WiFiClientSecure.h, swap the network client, and give it the CA certificate of your broker:

TLS changes
#include <WiFiClientSecure.h>

// PEM of the root CA that signed your broker's certificate
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the root CA certificate here...
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure net;
PubSubClient mqtt(net);

const uint16_t MQTT_PORT = 8883;

// in setup(), before mqtt.setServer():
net.setCACert(ROOT_CA);
// net.setInsecure();  // skips verification - local testing only!

Many public brokers use certificates from Let's Encrypt, whose root is ISRG Root X1; check your broker's documentation for the right one. TLS also needs the correct time for certificate validity checks on some setups, and it uses noticeably more RAM — an ESP32 handles it fine, an ESP8266 struggles. For the full picture, read our MQTT security best practices.

Want this sketch with your broker's host, port and credentials already filled in? The MQTT code generator generates it for your own broker settings.

Frequently asked questions

Why does PubSubClient not publish messages larger than 256 bytes?

PubSubClient uses a 256-byte buffer by default, and that buffer must hold the whole MQTT packet including the topic. publish() returns false when the packet does not fit. Call client.setBufferSize(1024) or larger before connecting.

Why does my ESP32 keep disconnecting from the MQTT broker?

The most common causes are a client ID that another device also uses, long delay() calls that stop client.loop() from running so keep-alive pings are missed, and weak WiFi. Use a unique client ID per device and keep loop() non-blocking.

Does PubSubClient support QoS 1 and MQTT 5?

PubSubClient speaks MQTT 3.1.1 only. It can subscribe with QoS 0 or 1 but publishes with QoS 0 only. If you need QoS 1 publishing or MQTT 5 features, look at libraries such as AsyncMqttClient or the ESP-IDF esp-mqtt component.