Tutorials

ESP8266 MQTT Tutorial with PubSubClient

Connect a NodeMCU or Wemos D1 mini to an MQTT broker with PubSubClient: full Arduino sketch, non-blocking reconnect loop, Last Will and TLS with fingerprints.

Updated · 9 min read

ESP8266 MQTT Tutorial with PubSubClient

The ESP8266 is still one of the cheapest ways to put a sensor on Wi-Fi, and PubSubClient is the MQTT library most ESP8266 sketches use. This tutorial gives you a complete sketch for a NodeMCU or Wemos D1 mini that connects to an MQTT broker, publishes telemetry, receives commands, reconnects without blocking and announces itself with a Last Will — then upgrades the connection to TLS with a certificate fingerprint and explains when to use a root CA instead.

Tested setup: Arduino IDE 2.x or arduino-cli, ESP8266 core 3.1.x, PubSubClient 2.8.

Install the toolchain

  1. In the Arduino IDE, open Preferences and add this board manager URL: https://arduino.esp8266.com/stable/package_esp8266com_index.json.
  2. Boards Manager → install esp8266 by ESP8266 Community.
  3. Library Manager → install PubSubClient by Nick O'Leary.
  4. Select NodeMCU 1.0 (ESP-12E Module) or LOLIN(WEMOS) D1 R2 & mini as the board.

With arduino-cli the same steps are:

arduino-cli setup
arduino-cli config add board_manager.additional_urls \
  https://arduino.esp8266.com/stable/package_esp8266com_index.json
arduino-cli core update-index
arduino-cli core install esp8266:esp8266
arduino-cli lib install PubSubClient

The ESP8266 only supports 2.4 GHz Wi-Fi. If your router broadcasts one combined SSID for both bands, that usually works; a 5 GHz-only network never will.

The complete sketch

esp8266_mqtt.ino
#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASS = "your-wifi-password";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = "username";
const char* MQTT_PASS = "password";

WiFiClient net;
PubSubClient mqtt(net);

String clientId, baseTopic;
unsigned long lastAttempt = 0, retryDelay = 1000, 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("[%s] %s\n", topic, msg.c_str());
  if (msg == "on")  digitalWrite(LED_BUILTIN, LOW);    // built-in LED is active-low
  if (msg == "off") digitalWrite(LED_BUILTIN, HIGH);
}

bool connectMqtt() {
  String status = baseTopic + "/status";
  // connect(id, user, pass, willTopic, willQos, willRetain, willMessage)
  if (!mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS,
                    status.c_str(), 1, true, "offline")) {
    Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
    return false;
  }
  mqtt.publish(status.c_str(), "online", true);              // retained
  mqtt.subscribe((baseTopic + "/led").c_str(), 1);
  Serial.println("MQTT connected as " + clientId);
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

  WiFi.persistent(false);              // don't rewrite flash on every boot
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  clientId  = "esp8266-" + String(ESP.getChipId(), HEX);
  baseTopic = "testmqtt/esp8266/" + clientId;

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);             // default is 256 bytes incl. topic and header
  mqtt.setKeepAlive(30);
  mqtt.setSocketTimeout(5);            // a failed connect blocks for at most ~5 s
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    delay(10);                         // core reconnects Wi-Fi on its own
    return;
  }

  if (!mqtt.connected()) {
    unsigned long now = millis();
    if (now - lastAttempt >= retryDelay) {
      lastAttempt = now;
      retryDelay = connectMqtt() ? 1000 : min(retryDelay * 2, 60000UL);
    }
    return;
  }

  mqtt.loop();                         // must run often: receives messages, sends pings

  if (millis() - lastPublish >= 10000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload),
             "{\"rssi\":%d,\"heap\":%u,\"uptime\":%lu}",
             WiFi.RSSI(), ESP.getFreeHeap(), millis() / 1000);
    mqtt.publish((baseTopic + "/telemetry").c_str(), payload);
  }
}

Why it is written this way

  • Non-blocking reconnect. The classic example uses while (!client.connected()) { ...; delay(5000); }, which freezes everything else — buttons, sensors, even the Wi-Fi stack — while the broker is unreachable. Here, loop() returns immediately and retries with exponential backoff (1 s, 2 s, 4 s … up to 60 s), so a fleet does not hammer a broker that is restarting.
  • Stable client ID from ESP.getChipId(). Two boards with the same ID kick each other off in an endless loop; see client ID best practices.
  • Last Will + retained status. If the board loses power, the broker publishes offline to .../status after the keep-alive expires (1.5 × 30 s here). Anything subscribing later sees the current state immediately.
  • Subscribe after every connect. PubSubClient uses a clean session, so subscriptions do not survive a reconnect. Shared test endpoints such as broker.connectmq.com do not keep sessions anyway, so the sketch always resubscribes in connectMqtt().
  • setBufferSize(512) — PubSubClient's default buffer is 256 bytes for the whole packet, including the topic. A long topic plus a JSON payload exceeds that quickly, and publish() then returns false without any other hint. The packet size calculator shows how big your packet really is.
  • setSocketTimeout(5) — connect() itself is blocking. The default timeout is 15 seconds, long enough to trigger watchdog resets or a frozen UI.

Test the sketch

Flash the board and open the Serial Monitor at 115200 baud. You should see MQTT connected as esp8266-xxxxxx. If you see state=-2 or another code instead, jump to the error table below.

Then open the online MQTT client, connect to broker.connectmq.com and subscribe to testmqtt/esp8266/#. The retained online status shows up first, then a telemetry JSON every 10 seconds. Publish on to testmqtt/esp8266/<client-id>/led and the LED lights up.

Watch your ESP8266 in the browser

To test the Last Will, unplug the board and wait about 45 seconds: the status topic flips to offline. Plug it back in and it returns to online — that is your reconnect path working.

Adding TLS with a fingerprint

Port 1883 sends the MQTT password in clear text. The ESP8266 core includes BearSSL, so TLS on port 8883 only needs a different client class. The lightest way to verify the server is certificate pinning by SHA-1 fingerprint.

Get the fingerprint of the broker's current certificate from your computer:

Read the SHA-1 fingerprint
openssl s_client -connect broker.connectmq.com:8883 \
  -servername broker.connectmq.com </dev/null 2>/dev/null \
  | openssl x509 -noout -fingerprint -sha1 \
  | sed 's/.*=//; s/:/ /g'

Then change three things in the sketch:

TLS changes
#include <WiFiClientSecure.h>

const uint16_t MQTT_PORT = 8883;
const char* FINGERPRINT = "AA BB CC DD EE FF 00 11 22 33 44 55 66 77 88 99 AA BB CC DD";

BearSSL::WiFiClientSecure net;         // replaces WiFiClient
PubSubClient mqtt(net);

// in setup(), before mqtt.setServer():
net.setFingerprint(FINGERPRINT);
mqtt.setSocketTimeout(10);             // the TLS handshake is slow on an 80 MHz chip

The handshake takes noticeably longer than a plain TCP connect and needs a good chunk of the ESP8266's limited RAM. Watch ESP.getFreeHeap() in the telemetry; if it gets tight, BearSSL can use smaller buffers with net.setBufferSizes(1024, 1024), but only when the broker supports the TLS max fragment length extension — check first with BearSSL::WiFiClientSecure::probeMaxFragmentLength(MQTT_HOST, 8883, 1024).

Fingerprint vs root CA

MethodHowTrade-off
setFingerprint()Pin the leaf certificate's SHA-1Smallest and fastest, but breaks on every certificate renewal
setTrustAnchors()Pin the root CA (BearSSL::X509List)Survives renewals; needs the correct time via configTime()
setInsecure()No verificationEncrypted but accepts any server — debugging only

Certificates from public CAs are renewed regularly, often every few months. A fingerprint-pinned fleet stops connecting on renewal day with state=-2, and you have to reflash every device. For anything long-lived, pin the root CA instead:

Root CA instead of a fingerprint
#include <time.h>
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...root certificate of the CA that signs your broker's certificate...
-----END CERTIFICATE-----
)EOF";
BearSSL::X509List trustAnchor(ROOT_CA);

// in setup(), after Wi-Fi is connected:
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
while (time(nullptr) < 1700000000) delay(200);   // wait for NTP
net.setTrustAnchors(&trustAnchor);

Without a valid clock, BearSSL considers every certificate expired or not yet valid. More TLS detail, including client certificates, is in the ESP32 TLS guide — the BearSSL API differs, but the concepts are the same.

Error codes

mqtt.state() returns these values:

stateConstantTypical cause
-4MQTT_CONNECTION_TIMEOUTNo ping response within keep-alive; weak Wi-Fi
-3MQTT_CONNECTION_LOSTNetwork dropped or broker closed the socket
-2MQTT_CONNECT_FAILEDTCP/TLS failed: wrong host or port, DNS, firewall, stale fingerprint
-1MQTT_DISCONNECTEDClean disconnect, or never connected
1MQTT_CONNECT_BAD_PROTOCOLBroker refuses MQTT 3.1.1
2MQTT_CONNECT_BAD_CLIENT_IDClient ID rejected
3MQTT_CONNECT_UNAVAILABLEBroker unavailable
4MQTT_CONNECT_BAD_CREDENTIALSWrong username or password
5MQTT_CONNECT_UNAUTHORIZEDAuthenticated but not allowed to connect

Positive values are CONNACK return codes from the broker — look them up in the reason codes tool. Negative values come from the library itself and almost always mean a network problem. The step-by-step method in testing an ESP32 broker connection works the same on an ESP8266.

Next steps

  • Moving to a newer chip? The ESP32 Arduino tutorial uses the same PubSubClient API.
  • Need QoS 1 publishing or a different API style? Compare with the arduino-mqtt library.
  • Building bigger payloads? ArduinoJson with MQTT shows how to size buffers correctly.

A reliable ESP8266 MQTT node comes down to a few habits: never block loop(), reconnect with backoff, resubscribe on every connect, size the buffer for your real packets, and pin a root CA rather than a fingerprint for devices that must keep working after the next certificate renewal.

Frequently asked questions

Why does my ESP8266 TLS connection stop working after a few weeks?

If you pinned the certificate with setFingerprint(), the fingerprint changes every time the broker renews its certificate, and the handshake then fails with state -2. Pin the root CA with setTrustAnchors() instead, which survives renewals as long as the same CA signs the new certificate.

Can PubSubClient publish with QoS 1 on the ESP8266?

No. PubSubClient publishes at QoS 0 only. It can subscribe with QoS 0 or 1, so incoming messages can be acknowledged, but outgoing messages are fire-and-forget. If you need QoS 1 publishes, use a library such as arduino-mqtt or add an application-level acknowledgement.

What does PubSubClient state -2 mean?

MQTT_CONNECT_FAILED: the TCP or TLS connection to the broker could not be opened. The problem is below MQTT — Wi-Fi, DNS, the wrong port, a firewall, or a TLS handshake failure such as a stale fingerprint or a plain WiFiClient on port 8883.