The ESP8266 is still one of the cheapest ways to put a sensor on Wi-Fi, and ESP8266 MQTT projects are a natural fit for its small footprint. In this tutorial you will connect a NodeMCU or Wemos D1 mini to a broker with PubSubClient, publish JSON telemetry, switch the onboard LED from your computer, and then upgrade to TLS without running out of memory.
What You Need
- A NodeMCU v2/v3, Wemos D1 mini or any ESP8266 board with USB.
- Arduino IDE 2.x with the "esp8266 by ESP8266 Community" board package (add
https://arduino.esp8266.com/stable/package_esp8266com_index.jsonunder File → Preferences → Additional boards manager URLs). - The PubSubClient library by Nick O'Leary from the Library Manager.
- The Mosquitto command-line tools on your computer for testing.
- A 2.4 GHz Wi-Fi network. The ESP8266 does not support 5 GHz.
For the first steps you will use broker.connectmq.com, the free public MQTT broker operated by ConnectMQ. It accepts anonymous clients on port 1883 with any non-empty, unique client ID. Treat it as a public notice board: everything you publish there can be read by others.
Board Settings That Matter
Before writing code, set these in the Tools menu:
| Setting | Recommended value | Why |
|---|---|---|
| Board | NodeMCU 1.0 (ESP-12E Module) or LOLIN(WEMOS) D1 mini | Correct flash layout and pin names |
| CPU Frequency | 160 MHz for TLS, 80 MHz otherwise | TLS handshakes are much faster at 160 MHz |
| SSL Support | All SSL ciphers (default) or Basic SSL ciphers | Basic saves flash, still works with modern brokers |
| lwIP Variant | v2 Lower Memory | Leaves more heap for your sketch |
| Upload Speed | 921600 (drop to 115200 if uploads fail) | Faster iteration |
Step 1: Your First ESP8266 MQTT Sketch
The sketch below uses the onboard LED on GPIO2 (LED_BUILTIN), which is active-low on most modules. It publishes uptime, signal strength and free heap every five seconds, and listens for ON or OFF commands.
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
char deviceId[24];
char topicTelemetry[64];
char topicLedSet[64];
char topicLedState[64];
WiFiClient netClient;
PubSubClient mqtt(netClient);
unsigned long lastPublish = 0;
unsigned long lastAttempt = 0;
void setLed(bool on) {
digitalWrite(LED_BUILTIN, on ? LOW : HIGH); // active-low
mqtt.publish(topicLedState, on ? "ON" : "OFF", true);
}
void onMessage(char* topic, byte* payload, unsigned int length) {
char msg[16];
unsigned int n = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1;
memcpy(msg, payload, n);
msg[n] = '\0';
Serial.printf("[%s] %s\n", topic, msg);
if (strcmp(msg, "ON") == 0) setLed(true);
if (strcmp(msg, "OFF") == 0) setLed(false);
}
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(300);
Serial.print(".");
}
Serial.printf(" connected, IP %s\n", WiFi.localIP().toString().c_str());
}
bool connectMqtt() {
if (mqtt.connect(deviceId, nullptr, nullptr, topicLedState, 1, true, "offline")) {
Serial.println("MQTT connected");
mqtt.subscribe(topicLedSet, 1);
setLed(false);
return true;
}
Serial.printf("MQTT failed, rc=%d\n", mqtt.state());
return false;
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
// Unique ID from the chip ID, e.g. esp8266-3f2a1b
snprintf(deviceId, sizeof(deviceId), "esp8266-%06x", ESP.getChipId());
snprintf(topicTelemetry, sizeof(topicTelemetry), "connectmq/demo/%s/telemetry", deviceId);
snprintf(topicLedSet, sizeof(topicLedSet), "connectmq/demo/%s/led/set", deviceId);
snprintf(topicLedState, sizeof(topicLedState), "connectmq/demo/%s/led/state", deviceId);
Serial.printf("\nDevice ID: %s\n", deviceId);
connectWifi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) {
if (millis() - lastAttempt > 5000) {
lastAttempt = millis();
connectMqtt();
}
return;
}
mqtt.loop();
if (millis() - lastPublish > 5000) {
lastPublish = millis();
char payload[96];
snprintf(payload, sizeof(payload), "{\"uptime\":%lu,\"rssi\":%d,\"heap\":%u}",
millis() / 1000, WiFi.RSSI(), ESP.getFreeHeap());
mqtt.publish(topicTelemetry, payload);
}
}
Open the serial monitor at 115200 baud and note the device ID, then subscribe from your computer (replace the ID with yours):
mosquitto_sub -h broker.connectmq.com -p 1883 -i laptop-3f2a1b -t "connectmq/demo/esp8266-3f2a1b/#" -v
Switch the LED:
mosquitto_pub -h broker.connectmq.com -p 1883 -i ctl-3f2a1b -t "connectmq/demo/esp8266-3f2a1b/led/set" -m ON
The led/state topic is retained, so any new subscriber immediately learns whether the LED is on, and the Last Will sets it to offline if the board loses power.
Step 2: Understand the ESP8266 Memory Budget
Once the plain sketch works, check the heap value it publishes. A simple sketch usually leaves a comfortable amount of free heap, but TLS changes the picture. A BearSSL session with default buffers needs a large contiguous block of memory for its receive and transmit buffers plus certificate validation work, and the ESP8266 has only a fraction of the RAM of an ESP32.
Practical consequences:
- Keep the PubSubClient buffer as small as your messages allow. Calling
setBufferSize(4096)"just in case" may be the difference between a working and a failing handshake. - Avoid heavy use of the
Stringclass in long-running sketches; repeated allocations fragment the heap and the TLS handshake may fail days later even though free heap looks fine.ESP.getMaxFreeBlockSize()shows the largest contiguous block, which is the number that matters. - Build JSON with
snprintfor a fixed-size ArduinoJson document rather than string concatenation.
If you regularly need large payloads, read PubSubClient buffer size for streaming techniques that avoid big buffers.
Step 3: ESP8266 MQTT over TLS
For production, your device should connect to a private broker over TLS on port 8883 with its own credentials. With a managed MQTT broker such as ConnectMQ, you get a username, password and client ID from the portal. The server certificate is issued by Let's Encrypt, so you trust the ISRG Root X1 root certificate.
The ESP8266 core uses BearSSL. Unlike the ESP32, it checks certificate validity dates, so the clock must be correct before the handshake.
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>
// ISRG Root X1 (Let's Encrypt root), paste the full PEM
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...ISRG Root X1...
-----END CERTIFICATE-----
)EOF";
BearSSL::X509List trustAnchors(ROOT_CA);
BearSSL::WiFiClientSecure tlsClient;
PubSubClient mqtt(tlsClient);
void syncClock() {
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
Serial.print("Waiting for NTP");
time_t now = time(nullptr);
while (now < 1700000000) { // any timestamp well in the past
delay(250);
Serial.print(".");
now = time(nullptr);
}
Serial.println(" done");
}
void setupTls() {
syncClock();
tlsClient.setTrustAnchors(&trustAnchors);
// Optional: smaller buffers if the server supports max fragment length
if (tlsClient.probeMaxFragmentLength("broker.connectmq.com", 8883, 1024)) {
tlsClient.setBufferSizes(1024, 1024);
}
mqtt.setServer("broker.connectmq.com", 8883);
}
bool connectMqtt() {
return mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD");
}
Call setupTls() in setup() after Wi-Fi connects. Set CPU frequency to 160 MHz; the first handshake can take a noticeable moment at 80 MHz, and your watchdog will not like a slow connect() inside a busy loop.
Step 4: Battery-Powered ESP8266 MQTT with Deep Sleep
Many ESP8266 sensors run on batteries and only need to report every few minutes. The radio is by far the biggest power consumer, so the winning pattern is: wake up, connect, publish, disconnect, sleep. Deep sleep on the ESP8266 requires a hardware link between GPIO16 (D0 on NodeMCU and D1 mini) and the RST pin, because the chip wakes up by resetting itself. Disconnect that wire while uploading if your board misbehaves during flashing.
void publishAndSleep() {
connectWifi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
if (mqtt.connect(deviceId)) {
char payload[64];
snprintf(payload, sizeof(payload), "{\"a0\":%d,\"rssi\":%d}", analogRead(A0), WiFi.RSSI());
mqtt.publish(topicTelemetry, payload);
// give the stack a moment to flush the packet before we cut power to the radio
for (int i = 0; i < 10; i++) { mqtt.loop(); delay(10); }
mqtt.disconnect();
}
ESP.deepSleep(5 * 60 * 1000000ULL); // 5 minutes, in microseconds
}
Call publishAndSleep() at the end of setup() and leave loop() empty. A few practical notes: a TLS handshake on every wake-up costs noticeably more time and energy than plain MQTT, so weigh the security requirement against battery life and consider longer sleep intervals with TLS. Use a retained message for the latest reading, so dashboards that open between wake-ups still see a value. And do not use a Last Will for presence on sleepy devices; a clean disconnect() never triggers it, and an "offline" status would be misleading anyway.
Troubleshooting ESP8266 MQTT Problems
- rc=-2 on TLS but fine on 1883: clock not synced, wrong root certificate, or not enough contiguous heap. Print
tlsClient.getLastSSLError()for the BearSSL reason. Our PubSubClient rc=-2 guide covers every code. - Random resets (
wdt reset): something blocks too long. Never put longdelay()calls in the MQTT callback, and callyield()in long loops. - Board never joins Wi-Fi: 5 GHz-only network, wrong password, or a captive portal on a guest network.
- Board disconnects whenever a second board connects: both boards use the same client ID. The chip-ID approach above prevents this.
- Messages over a few hundred bytes vanish: the default 256-byte PubSubClient buffer. Increase it moderately.
- Messages stop arriving after hours: heap fragmentation. Watch
getMaxFreeBlockSize()over time.
ESP8266 or ESP32?
The ESP8266 is perfectly capable for single-sensor nodes, relays and small displays, especially on plain MQTT or occasional TLS connections. If you need many concurrent TLS sessions, Bluetooth, more GPIO or room for OTA updates plus large payloads, move to the ESP32. Our ESP32 vs ESP8266 comparison goes through the trade-offs, and the ESP32 IoT tutorial is the next step when you outgrow this board.
Run Your ESP8266 Fleet on ConnectMQ
When the prototype works, move it to a cloud MQTT broker with private credentials and TLS. ConnectMQ supports MQTT 3.1.1 and 5.0 on TCP 1883 and TLS 8883, and the portal includes an in-browser MQTT client and live topic explorer for checking what your boards publish. Create a free Serverless broker at app.connectmq.com, no credit card required.
Frequently Asked Questions
Can the ESP8266 use MQTT over TLS?
Yes. The ESP8266 core ships BearSSL, and PubSubClient works with BearSSL::WiFiClientSecure. Sync the clock first, trust the broker's root certificate and keep other memory use modest.
Which pins does the NodeMCU onboard LED use?
On most NodeMCU and Wemos D1 mini boards it is GPIO2 (LED_BUILTIN, also labelled D4), and it is active-low, so writing LOW turns it on.
Can I use the ESP8266 with MQTT 5?
PubSubClient only supports MQTT 3.1.1, which is enough for most sensors. The broker can still serve MQTT 5 clients, such as dashboards and backends, on the same topics.
Why does my ESP8266 need the time for TLS?
BearSSL checks that the server certificate is within its validity period. Without a correct clock the device thinks it is 1970 and rejects the certificate.



