Connecting a cloud MQTT broker and an ESP32 over TLS is the step that turns a bench prototype into something you can deploy outside your home network. In this tutorial you will move an ESP32 sketch from a local Mosquitto broker on port 1883 to a ConnectMQ Serverless broker on port 8883, with proper certificate validation, credentials, a status topic and reliable reconnects.
What Changes When You Leave the Local Broker
Most ESP32 projects start with Mosquitto on a Raspberry Pi or laptop: plain TCP on 1883, no authentication, reachable only on your LAN. Moving to a cloud MQTT broker changes four things:
| Aspect | Local broker | Cloud broker |
|---|---|---|
| Host | 192.168.1.20 |
broker.connectmq.com |
| Port | 1883 (plain TCP) | 8883 (TLS) |
| Authentication | Often none | Username, password, client ID |
| Network client | WiFiClient |
WiFiClientSecure |
| Certificate | None | Server certificate validated against a CA |
| Reachability | LAN only | Anywhere with internet |
The MQTT logic itself, topics, publishes and subscriptions, stays the same. If you have never connected an ESP32 to MQTT before, start with the ESP32 MQTT tutorial and come back here.
Prerequisites
- An ESP32 development board and USB cable.
- Arduino IDE 2.x or PlatformIO with the ESP32 Arduino core installed (see the official Espressif Arduino core documentation).
- The PubSubClient library by Nick O'Leary.
- A free Serverless broker. Sign up at the portal, choose the Serverless (Free) plan, create a broker and copy the generated username, password and client ID.
Step 1: Get the Root CA Certificate
The broker serves a Let's Encrypt certificate. The ESP32 must trust the root that signed the chain. Let's Encrypt chains up to ISRG Root X1, so that is the certificate to embed. You can confirm the chain yourself from any machine with OpenSSL:
openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -showcerts </dev/null
Look at the issuer lines at the top of the output. Then download ISRG Root X1 in PEM format from Let's Encrypt's certificate page and paste it into your sketch as a raw string. Embedding the long-lived root rather than the server's own certificate means you do not need to reflash devices when the server certificate renews every few months.
Step 2: The Complete Sketch
This sketch connects to Wi-Fi, syncs time (TLS certificate validation needs a correct clock), connects to the broker over TLS with credentials, publishes a retained online status with a Last Will, subscribes to a command topic and publishes telemetry every ten seconds.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>
// ---- Wi-Fi ----
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
// ---- ConnectMQ ----
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";
const char* TOPIC_TELEMETRY = "devices/esp32-01/telemetry";
const char* TOPIC_STATUS = "devices/esp32-01/status";
const char* TOPIC_COMMAND = "devices/esp32-01/cmd";
// ISRG Root X1 (paste the full PEM from Let's Encrypt here)
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
PASTE_ISRG_ROOT_X1_PEM_CONTENT_HERE
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure net;
PubSubClient mqtt(net);
unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;
unsigned long reconnectDelay = 1000;
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nWi-Fi connected, IP %s\n", WiFi.localIP().toString().c_str());
}
void syncTime() {
configTime(0, 0, "pool.ntp.org", "time.google.com");
Serial.print("Syncing time");
time_t now = time(nullptr);
while (now < 1700000000) {
delay(500);
Serial.print(".");
now = time(nullptr);
}
Serial.println(" done");
}
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("Command on %s: %s\n", topic, msg.c_str());
if (msg == "led:on") digitalWrite(LED_BUILTIN, HIGH);
if (msg == "led:off") digitalWrite(LED_BUILTIN, LOW);
}
bool connectMqtt() {
Serial.print("Connecting to ConnectMQ... ");
bool ok = mqtt.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS,
TOPIC_STATUS, 1, true, "offline");
if (!ok) {
Serial.printf("failed, state=%d\n", mqtt.state());
return false;
}
Serial.println("connected");
mqtt.publish(TOPIC_STATUS, "online", true);
mqtt.subscribe(TOPIC_COMMAND, 1);
return true;
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
connectWiFi();
syncTime();
net.setCACert(ROOT_CA);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
mqtt.setBufferSize(512);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (!mqtt.connected()) {
unsigned long now = millis();
if (now - lastReconnectAttempt > reconnectDelay) {
lastReconnectAttempt = now;
if (connectMqtt()) {
reconnectDelay = 1000;
} else {
reconnectDelay = min(reconnectDelay * 2, 60000UL);
}
}
return;
}
mqtt.loop();
if (millis() - lastPublish > 10000) {
lastPublish = millis();
char payload[128];
snprintf(payload, sizeof(payload),
"{\"uptime_s\":%lu,\"rssi\":%d,\"heap\":%u}",
millis() / 1000, WiFi.RSSI(), ESP.getFreeHeap());
mqtt.publish(TOPIC_TELEMETRY, payload);
Serial.println(payload);
}
}
Notes on the choices:
- Retained status plus Last Will. The broker publishes
offlineon your behalf if the ESP32 drops without a clean disconnect, and new subscribers always see the last status. More on this pattern in ESP32 MQTT Last Will. - Non-blocking backoff. Reconnect attempts double from one second up to a minute, so a fleet does not hammer the broker after an outage.
- Buffer size. PubSubClient's default 256-byte buffer silently drops larger messages; 512 bytes gives headroom.
- Some boards lack
LED_BUILTIN. Define it as your board's LED pin, often GPIO 2.
Step 3: Verify from Your Computer
Open the portal's in-browser client or topic explorer and subscribe to devices/#. You should see telemetry every ten seconds and online retained on the status topic. To send a command from the command line:
mosquitto_pub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
-u YOUR_USERNAME -P YOUR_PASSWORD -i YOUR_CLIENT_ID-cli \
-t devices/esp32-01/cmd -m 'led:on' -q 1
Use a different client ID from the ESP32's. If your plan binds a single client ID to the credentials, use the portal's in-browser client instead, or create additional credentials for tools.
Testing Without an Account
To try the sketch before signing up, use the free public MQTT broker: keep port 8883 and the same root CA, pass empty username and password (mqtt.connect(clientId)), choose any non-empty unique client ID, and prefix every topic with something unique such as connectmq/demo/x8q2m/esp32-01/.... The public area is readable by everyone, so do not publish anything sensitive, and note its limits: 64 KB per message and 10 messages per second per client.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
state=-2 immediately |
TLS handshake failed | Check root CA, check time sync, check port 8883 |
| Hangs at time sync | NTP blocked | Allow UDP 123 or use a reachable NTP server |
state=4 |
Bad username or password | Copy credentials again from the portal |
state=5 |
Not authorized | Client ID does not match issued credentials |
| Connects then drops in a loop | Duplicate client ID | Ensure each device and tool uses its own ID |
| Large messages never arrive | PubSubClient buffer too small | Raise setBufferSize |
| Works at home, fails on site | Outbound 8883 blocked | Ask IT to open 8883, or use WSS on 8084 with an MQTT-over-WebSocket client |
During development only, net.setInsecure() skips certificate validation and helps isolate whether the CA is the problem. Never ship firmware with it. For deeper TLS issues, the ESP32 MQTT SSL guide goes further.
Memory, Power and Certificate Lifecycle
TLS on an ESP32 is comfortable but not free, and a few design decisions avoid trouble later.
Heap usage. A TLS session needs a noticeable chunk of RAM for buffers and the handshake. On a plain ESP32 that is fine for one connection, but if your sketch also runs a web server, a camera or large JSON documents, watch ESP.getFreeHeap() in the telemetry payload above. A steadily falling value usually points to a leak in your own code, such as String concatenation inside a loop, rather than in the TLS stack.
Keep the connection open. The handshake is the expensive part. Connecting, publishing one message and disconnecting every few seconds costs far more energy and time than holding one session open with a 30 to 60 second keep alive.
Battery devices. If the board deep-sleeps between readings, each wake-up pays for Wi-Fi association, NTP and a TLS handshake. Reduce the cost by waking less often and batching several readings into one publish, and consider storing the last NTP time in RTC memory so you can skip a full sync on short sleeps.
Root certificate expiry. Embedding the root CA rather than the server's leaf certificate means routine renewals never touch your devices. Roots do eventually change, though, so keep a way to update the certificate in the field, either through OTA firmware updates or by storing the PEM in flash where a provisioning flow can replace it.
Production Checklist
- Store credentials in NVS or provision them at manufacturing, not in source control.
- Give each device its own client ID.
- Keep the root CA rather than a leaf certificate.
- Use reconnect backoff with jitter.
- Publish a retained status with a Last Will.
- Keep payloads small and structured.
- Watch the devices in the portal dashboard and set threshold alert rules for key metrics.
The broader security picture is in MQTT security best practices.
Put Your ESP32 on ConnectMQ
Create a free Serverless broker at app.connectmq.com with no credit card, paste the credentials into the sketch above and your ESP32 is talking to the cloud over TLS. ConnectMQ is a managed MQTT broker, so certificates, patching and capacity are handled for you while you focus on firmware.
Frequently Asked Questions
Why does my ESP32 need the correct time for TLS?
Certificate validation checks validity dates. Without NTP the ESP32 clock starts in 1970, every certificate looks invalid and the handshake fails.
Can I use AsyncMqttClient instead of PubSubClient?
Yes. AsyncMqttClient is non-blocking and supports TLS depending on your core and build options, while PubSubClient with WiFiClientSecure is the simplest widely used setup.
Which root certificate should I embed for ConnectMQ?
ISRG Root X1, the Let's Encrypt root. It is long-lived, so server certificate renewals do not require firmware updates.
Does TLS slow the ESP32 down noticeably?
The handshake takes a moment and uses extra RAM, but once connected the overhead per message is small. Keep the connection open instead of reconnecting for every publish.



