This ESP32 MQTT tutorial takes you from an empty Arduino sketch to a board that publishes JSON telemetry, listens for commands and switches its onboard LED. You will first connect to the free public area of broker.connectmq.com, then upgrade the same sketch to TLS on port 8883 with credentials from the ConnectMQ portal. Every step is verified with mosquitto_sub and mosquitto_pub from your computer.
What You Will Build
The finished device does three things:
- Connects to Wi-Fi and to an MQTT broker, with automatic reconnects.
- Publishes a JSON message such as
{"temperature":23.41,"uptime":125}every five seconds. - Subscribes to a command topic and turns the onboard LED on or off when it receives
ON,OFForTOGGLE.
The topic layout keeps everything under one unique prefix:
| Topic | Direction | Example payload |
|---|---|---|
connectmq/demo/<deviceId>/telemetry |
ESP32 publishes | {"temperature":23.41,"uptime":125} |
connectmq/demo/<deviceId>/led/set |
ESP32 subscribes | ON, OFF, TOGGLE |
connectmq/demo/<deviceId>/led/state |
ESP32 publishes (retained) | ON or OFF |
If MQTT terms like topic, QoS or retained message are new to you, read What Is an MQTT Broker? first.
Prerequisites
- An ESP32 development board (any ESP32 DevKit works; most have a blue LED on GPIO 2).
- Arduino IDE 2.x with the esp32 board package installed through the Boards Manager.
- The PubSubClient library by Nick O'Leary, installed through the Library Manager.
- A 2.4 GHz Wi-Fi network (the ESP32 does not support 5 GHz).
- Optional: the Mosquitto command-line clients (
mosquitto_sub,mosquitto_pub) on your computer for testing. - Optional: a DHT22 sensor. The sketch simulates temperature by default, so no hardware is required.
Broker Endpoints for ESP32 MQTT
broker.connectmq.com exposes four listeners. The ESP32 uses the first two; the WebSocket ports are for browsers.
| Protocol | Port | Use in this tutorial |
|---|---|---|
| MQTT (TCP) | 1883 | Part 1, anonymous public area |
| MQTT over TLS | 8883 | Part 2, portal credentials |
| WebSocket | 8083 | Not used (browser clients) |
| Secure WebSocket | 8084 | Not used (browser clients) |
The public area needs no account: connect with no username or password and a non-empty, unique client ID. Keep in mind that anything published there is readable by anyone, so only send demo data. The public area allows 64 KB per message, 10 messages per second and 10 subscriptions per client, and up to 8 topic levels, which is plenty for this project. See the free public MQTT broker guide for details.
Part 1: ESP32 MQTT over Plain TCP (Port 1883)
Generate a unique client ID from the MAC address
Client IDs must be unique: if two clients connect with the same ID, the broker disconnects the older one. Every ESP32 has a factory-programmed MAC address, which makes a perfect source for a stable, unique ID. The sketch builds something like esp32-a1b2c3d4e5f6 and reuses it in the topic prefix.
The complete sketch
Create a new sketch, paste the code below and set your Wi-Fi SSID and password.
#include <WiFi.h>
#include <PubSubClient.h>
// ---- Wi-Fi ----
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
// ---- MQTT ----
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const int LED_PIN = 2; // onboard LED on most ESP32 DevKits
const unsigned long PUBLISH_INTERVAL_MS = 5000;
WiFiClient netClient;
PubSubClient mqtt(netClient);
String deviceId;
String topicTelemetry, topicLedSet, topicLedState;
unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;
bool ledOn = false;
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print("\nWi-Fi connected, IP: ");
Serial.println(WiFi.localIP());
}
String makeDeviceId() {
uint8_t mac[6];
WiFi.macAddress(mac);
char buf[20];
snprintf(buf, sizeof(buf), "esp32-%02x%02x%02x%02x%02x%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(buf);
}
void publishLedState() {
mqtt.publish(topicLedState.c_str(), ledOn ? "ON" : "OFF", true); // retained
}
void onMessage(char* topic, byte* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
msg.trim();
Serial.printf("Message on %s: %s\n", topic, msg.c_str());
if (String(topic) != topicLedSet) return;
if (msg.equalsIgnoreCase("ON")) ledOn = true;
else if (msg.equalsIgnoreCase("OFF")) ledOn = false;
else if (msg.equalsIgnoreCase("TOGGLE")) ledOn = !ledOn;
else return; // ignore anything else
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
publishLedState();
}
bool connectMqtt() {
Serial.printf("Connecting to MQTT as %s ... ", deviceId.c_str());
if (mqtt.connect(deviceId.c_str())) { // no username/password: public area
Serial.println("connected");
mqtt.subscribe(topicLedSet.c_str(), 1);
publishLedState();
return true;
}
Serial.printf("failed, state=%d\n", mqtt.state());
return false;
}
float readTemperature() {
// Simulated sensor: 22-25 °C with a little noise
return 23.5 + 1.5 * sin(millis() / 60000.0) + random(-20, 20) / 100.0;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
connectWiFi();
deviceId = makeDeviceId();
String base = "connectmq/demo/" + deviceId;
topicTelemetry = base + "/telemetry";
topicLedSet = base + "/led/set";
topicLedState = base + "/led/state";
Serial.println("Topic prefix: " + base);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setBufferSize(512); // default is 256 bytes, see below
mqtt.setKeepAlive(30);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (!mqtt.connected()) {
unsigned long now = millis();
if (now - lastReconnectAttempt > 5000) { // non-blocking retry every 5 s
lastReconnectAttempt = now;
if (connectMqtt()) lastReconnectAttempt = 0;
}
return;
}
mqtt.loop(); // process incoming messages and keepalive
if (millis() - lastPublish >= PUBLISH_INTERVAL_MS) {
lastPublish = millis();
char json[96];
snprintf(json, sizeof(json), "{\"temperature\":%.2f,\"uptime\":%lu}",
readTemperature(), millis() / 1000);
mqtt.publish(topicTelemetry.c_str(), json);
Serial.printf("Published %s\n", json);
}
}
Select your board and port, upload, and open the Serial Monitor at 115200 baud. You should see the IP address, the topic prefix and a Published line every five seconds. Copy the device ID from the topic prefix; you will need it in the next step.
How the reconnect logic works
The sketch never blocks forever waiting for the broker. If the connection drops, loop() tries again every five seconds while the rest of the program keeps running. Two details matter:
mqtt.loop()must run often. It reads incoming packets and sends keepalive pings; if you calldelay(10000)in your loop, the broker will consider the client dead.- After reconnecting, the sketch subscribes again. With the default clean session, subscriptions do not survive a disconnect, so resubscribing in
connectMqtt()is essential.
mqtt.state() returns a numeric code on failure: -2 usually means a network problem (wrong host, no internet), while positive values come from the broker's CONNACK, for example 2 for an identifier rejected.
Using a real DHT22 sensor
To use real data, install the "DHT sensor library" by Adafruit, wire the sensor's data pin to GPIO 4, and replace readTemperature():
#include <DHT.h>
DHT dht(4, DHT22); // call dht.begin() in setup()
float readTemperature() {
float t = dht.readTemperature();
return isnan(t) ? -127.0 : t; // send a sentinel instead of NaN
}
Verify with mosquitto_sub and mosquitto_pub
Open a terminal and subscribe to everything under your device's prefix. Replace esp32-a1b2c3d4e5f6 with the ID from the Serial Monitor.
mosquitto_sub -h broker.connectmq.com -p 1883 -i laptop-watch-81f3 \
-t "connectmq/demo/esp32-a1b2c3d4e5f6/#" -v
Output looks like this:
connectmq/demo/esp32-a1b2c3d4e5f6/led/state OFF
connectmq/demo/esp32-a1b2c3d4e5f6/telemetry {"temperature":23.41,"uptime":125}
connectmq/demo/esp32-a1b2c3d4e5f6/telemetry {"temperature":23.47,"uptime":130}
Now switch the LED from a second terminal:
mosquitto_pub -h broker.connectmq.com -p 1883 -i laptop-cmd-81f3 \
-t "connectmq/demo/esp32-a1b2c3d4e5f6/led/set" -q 1 -m "TOGGLE"
The LED changes and the subscriber prints the new retained led/state. Because the state is retained, any dashboard that subscribes later immediately receives the current value. The subscription uses QoS 1 so the command is acknowledged by the broker; MQTT QoS levels explained covers the trade-offs.
You can also watch the device come and go: subscribe to $SYS/broker/connection/esp32-a1b2c3d4e5f6/state and you will see 1 when it connects and 0 when it disconnects.
The PubSubClient Buffer Size
PubSubClient allocates a single buffer for both incoming and outgoing packets, and by default it is only 256 bytes. That buffer must hold the whole MQTT packet: fixed header, topic name and payload. A long topic plus a moderately sized JSON document exceeds it quickly, and when that happens publish() silently returns false and incoming messages that do not fit are dropped.
Call setBufferSize() before connecting and check the return value of publish():
| Payload size (approx.) | Suggested setBufferSize() |
|---|---|
| Up to ~150 bytes | 256 (default) |
| Up to ~400 bytes | 512 |
| Up to ~1,800 bytes | 2048 |
| Larger documents | Split the message or send only changed fields |
Each extra byte comes out of the ESP32's RAM, so size it for your real messages rather than the broker maximum.
Part 2: Secure ESP32 MQTT with TLS on Port 8883
The public area is perfect for learning, but production devices should use TLS and their own credentials. Sign in to https://app.connectmq.com, create a broker, and copy the generated username, password and client ID. Authenticated clients land in your own isolated tenant instead of the public area.
Client IDs on authenticated connections are restricted to the issued ID and its _suffix variants. Give each device its own suffix, for example YOUR_CLIENT_ID_esp32a, and your laptop another one.
Changes to the sketch
Only the network client, port, credentials and topics change. Download the ISRG Root X1 certificate (isrgrootx1.pem) from Let's Encrypt; the broker's certificate chains up to it. Paste its contents into the sketch:
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 8883;
const char* MQTT_USER = "YOUR_USERNAME";
const char* MQTT_PASS = "YOUR_PASSWORD";
const char* MQTT_CLIENT_ID = "YOUR_CLIENT_ID_esp32a";
// ISRG Root X1 (paste the full PEM from isrgrootx1.pem)
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the certificate body here...
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure netClient;
PubSubClient mqtt(netClient);
void syncClock() {
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
time_t now = time(nullptr);
while (now < 1700000000) { delay(250); now = time(nullptr); }
}
// in setup(), after connectWiFi():
// syncClock();
// netClient.setCACert(ROOT_CA);
// mqtt.setServer(MQTT_HOST, MQTT_PORT);
// topic base: "plant1/" + String(MQTT_CLIENT_ID) instead of "connectmq/demo/..."
bool connectMqtt() {
if (mqtt.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS)) {
mqtt.subscribe(topicLedSet.c_str(), 1);
publishLedState();
return true;
}
return false;
}
A few notes:
setCACert()makes the ESP32 verify that it is really talking tobroker.connectmq.com. Syncing the clock with NTP first lets the TLS stack check certificate validity dates.setInsecure()skips verification entirely. It is handy for a five-minute test, but it removes protection against man-in-the-middle attacks, so never ship it.- TLS uses noticeably more RAM and the first handshake takes a moment. That is normal on the ESP32.
- Keep credentials out of your Git repository, for example in a separate
secrets.hlisted in.gitignore. The MQTT security checklist covers device secret storage and other hardening steps.
Verify the secure connection
Subscribe with your own credentials and a different suffix. On Linux you can use the system CA store with --capath /etc/ssl/certs; on any OS you can pass the downloaded root certificate:
mosquitto_sub -h broker.connectmq.com -p 8883 --cafile isrgrootx1.pem \
-u YOUR_USERNAME -P YOUR_PASSWORD -i YOUR_CLIENT_ID_laptop \
-t "plant1/#" -v
Send a command the same way:
mosquitto_pub -h broker.connectmq.com -p 8883 --cafile isrgrootx1.pem \
-u YOUR_USERNAME -P YOUR_PASSWORD -i YOUR_CLIENT_ID_cli \
-t "plant1/YOUR_CLIENT_ID_esp32a/led/set" -q 1 -m "ON"
You can also open the in-browser MQTT client or the live topic explorer in the ConnectMQ portal to watch the telemetry without installing anything.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
state=-2 in a loop |
No internet, wrong host or blocked port | Check Wi-Fi, try port 1883, check firewall |
state=2 |
Client ID rejected | Use a non-empty unique ID; with credentials use the issued ID or a _suffix variant |
state=4 or 5 |
Bad credentials | Re-copy username and password from the portal |
| Publishes stop, no error shown | Message bigger than the buffer | Increase setBufferSize(), check publish() return value |
| Constant disconnects | mqtt.loop() not called often, or another client with the same ID |
Remove long delay() calls, give each client its own ID |
| TLS fails, plain TCP works | Wrong CA, clock not set | Use ISRG Root X1, sync time with NTP before connecting |
Get Started with ConnectMQ
You now have an ESP32 that streams telemetry and takes commands over MQTT, first on the public area and then over TLS with its own credentials. When you are ready to move beyond demo data, create a broker on the free Serverless plan at https://app.connectmq.com — no credit card required — and use the portal's dashboard, devices view and threshold alerts to keep an eye on your fleet.
Frequently Asked Questions
Which MQTT library should I use on the ESP32 with Arduino IDE?
PubSubClient is the most widely used choice: it is small, stable and works with both WiFiClient and WiFiClientSecure. It supports MQTT 3.1.1 with QoS 0 publishing and QoS 0 or 1 subscriptions, which covers most sensor projects.
Why does my ESP32 publish fail with no error?
Most often the packet is larger than PubSubClient's 256-byte default buffer. Call mqtt.setBufferSize(512) or larger before connecting and check the boolean returned by publish().
Do I need an account to test ESP32 MQTT with broker.connectmq.com?
No. Connect to port 1883 or 8883 without a username and with a unique client ID to use the public area. For real data, create a free account and use the generated credentials.
Can I use setInsecure() with WiFiClientSecure?
Only for quick tests. It encrypts traffic but skips server verification, so an attacker could impersonate the broker. Use setCACert() with ISRG Root X1 in anything you deploy.
How do I give multiple ESP32 boards their own client IDs on one broker?
Append a suffix to the client ID issued in the portal, such as YOUR_CLIENT_ID_esp32a and YOUR_CLIENT_ID_esp32b. ConnectMQ accepts the issued ID and its _suffix variants on authenticated connections.



