ESP32 and Arduino

PubSubClient Arduino Library Tutorial: Setup, Callbacks, Buffers and Keep Alive

A practical PubSubClient tutorial for ESP32 and ESP8266: install the library, connect to an MQTT broker, handle callbacks, size the buffer, tune keep alive and move to TLS with real credentials.

Microcontroller board running the PubSubClient library and exchanging MQTT messages with a cloud broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

PubSubClient is the most widely used MQTT library for Arduino-compatible boards, and it is small enough to run on almost anything with a network stack. In this tutorial you will install it, connect an ESP32 to a real broker, publish and subscribe, and learn the three settings that cause most field problems: buffer size, keep alive and socket timeout. By the end you will have a reconnect-safe sketch you can reuse in every project.

What PubSubClient Is (and What It Is Not)

PubSubClient, written by Nick O'Leary, is a lightweight MQTT 3.1.1 client. It does not own the network connection. Instead, it wraps any object that implements Arduino's Client interface: WiFiClient on the ESP32 and ESP8266, EthernetClient on a wired shield, or WiFiClientSecure when you want TLS. That design is why the same library runs on so many boards.

It is also deliberately minimal. Knowing the limits up front saves hours of debugging:

Feature PubSubClient behaviour
Protocol version MQTT 3.1.1 (MQTT 3.1 optional at compile time), no MQTT 5
Publish QoS QoS 0 only
Subscribe QoS QoS 0 or QoS 1
Default packet buffer 256 bytes (topic + payload + header)
Default keep alive 15 seconds
Default socket timeout 15 seconds
Last Will Supported on connect()
Retained publish Supported
Threading model Single-threaded, you must call loop() often

If you need MQTT 5 properties or QoS 2 publishing from the device, look at a different client. For the vast majority of sensors and actuators, QoS 0 telemetry plus QoS 1 command subscriptions are exactly what you want. Our guide to MQTT QoS levels explains why that combination works well.

Install PubSubClient and Prepare the Board

  1. In the Arduino IDE open Tools → Manage Libraries, search for "PubSubClient" and install the library by Nick O'Leary (version 2.8 or newer).
  2. Install the board package you need: "esp32 by Espressif Systems" for ESP32 boards or "esp8266 by ESP8266 Community" for NodeMCU-style boards.
  3. Select your board and port, then upload the empty Blink sketch once to confirm the toolchain works.
  4. Pick a unique device ID, for example pubsub-7f3a9c. You will use it as both the MQTT client ID and the topic prefix.

For the first test you will use the public area of broker.connectmq.com, the free public MQTT broker run by ConnectMQ. It accepts anonymous connections on port 1883 as long as the client ID is not empty. Everything published there is readable by anyone, so never send real credentials or personal data to it.

The Complete PubSubClient Example

The sketch below connects to Wi-Fi, connects to the broker with a Last Will, publishes a JSON message every five seconds, subscribes to a command topic and reconnects without blocking the rest of your code.

#include <WiFi.h>          // use <ESP8266WiFi.h> on ESP8266
#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;
const char* DEVICE_ID = "pubsub-7f3a9c";   // make this unique

char topicTelemetry[64];
char topicCommand[64];
char topicStatus[64];

WiFiClient netClient;
PubSubClient mqtt(netClient);

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

void onMessage(char* topic, byte* payload, unsigned int length) {
  // payload is NOT null-terminated: copy it with the length
  char msg[64];
  unsigned int n = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1;
  memcpy(msg, payload, n);
  msg[n] = '\0';

  Serial.printf("Message on %s: %s\n", topic, msg);

  if (strcmp(msg, "ping") == 0) {
    mqtt.publish(topicStatus, "pong");
  }
}

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.printf("\nIP address: %s\n", WiFi.localIP().toString().c_str());
}

bool connectMqtt() {
  // connect(clientId, user, pass, willTopic, willQos, willRetain, willMessage)
  bool ok = mqtt.connect(DEVICE_ID, nullptr, nullptr,
                         topicStatus, 1, true, "offline");
  if (ok) {
    mqtt.publish(topicStatus, "online", true);
    mqtt.subscribe(topicCommand, 1);
    Serial.println("MQTT connected");
  } else {
    Serial.printf("MQTT connect failed, rc=%d\n", mqtt.state());
  }
  return ok;
}

void setup() {
  Serial.begin(115200);

  snprintf(topicTelemetry, sizeof(topicTelemetry), "connectmq/demo/%s/telemetry", DEVICE_ID);
  snprintf(topicCommand,   sizeof(topicCommand),   "connectmq/demo/%s/cmd", DEVICE_ID);
  snprintf(topicStatus,    sizeof(topicStatus),    "connectmq/demo/%s/status", DEVICE_ID);

  connectWifi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);   // room for larger JSON payloads
  mqtt.setKeepAlive(30);     // seconds
  mqtt.setSocketTimeout(10); // seconds
}

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

  if (!mqtt.connected()) {
    unsigned long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      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());
    if (!mqtt.publish(topicTelemetry, payload)) {
      Serial.println("Publish failed");
    }
  }
}

Watch the output from your computer with the Mosquitto command-line tools:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/#" -v

Then send a command to the board:

mosquitto_pub -h broker.connectmq.com -p 1883 -i sender-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/cmd" -m "ping"

You should see pong appear on the status topic within a second.

How PubSubClient Callbacks Work

The callback signature is fixed: void callback(char* topic, byte* payload, unsigned int length). Three details matter.

First, the payload is a pointer into the library's internal buffer and it is not null-terminated. Treating it as a C string with Serial.println((char*)payload) prints garbage after the real message. Always copy using length, as the example does.

Second, the callback runs inside mqtt.loop(). If you publish from inside the callback, the library reuses the same buffer, so the original topic and payload pointers are no longer valid afterwards. Copy anything you need before you call publish().

Third, keep callbacks short. A delay(2000) inside the callback stops loop() from servicing the connection and can trigger a keep alive timeout. Set a flag or store the value, then act on it in your main loop.

Buffer Size: The Most Common PubSubClient Trap

Since version 2.8 you can call setBufferSize() at runtime instead of editing MQTT_MAX_PACKET_SIZE in the library header. The buffer must hold the whole MQTT packet: fixed header, topic length, topic, and payload. With the 256-byte default, a 220-character JSON document on a 40-character topic simply fails, and publish() returns false without any further explanation. Incoming messages that are too large are silently discarded before your callback is ever called.

Rules of thumb:

  • Size the buffer for your largest topic plus payload, plus about 10 bytes of header.
  • Call setBufferSize() before connect(), and check its return value: it returns false if the allocation failed.
  • On the ESP8266 keep the buffer modest, because RAM is shared with TLS and Wi-Fi.

We go into detail, including streaming large payloads with beginPublish(), in PubSubClient buffer size: fixing large message failures.

Keep Alive and Socket Timeout

Keep alive tells the broker how long the client may stay silent. PubSubClient sends a PINGREQ when nothing else has been sent during the interval, but only if loop() is being called. If your code blocks for longer than roughly one and a half times the keep alive interval, the broker drops the session and your Last Will fires.

  • setKeepAlive(30) is a sensible default for mains-powered devices on decent Wi-Fi.
  • Go higher (60 to 120 seconds) on cellular links where every packet costs energy or data.
  • setSocketTimeout(10) limits how long connect() and reads may block. Lower values keep a flaky network from freezing your device for 15 seconds at a time.

Reading Connection State

When connect() fails, mqtt.state() tells you why. Negative values are network-level problems; positive values are refusals from the broker.

state() Meaning
-4 Connection timeout, no CONNACK in time
-3 Connection lost
-2 Network connect failed (DNS, port, TLS, routing)
-1 Disconnected cleanly
0 Connected
1 Bad protocol version
2 Client ID rejected
3 Server unavailable
4 Bad username or password
5 Not authorised

The most common one by far is -2. Our dedicated guide on PubSubClient rc=-2 and other state codes walks through every value with fixes.

Moving to TLS and Real Credentials

The public broker is perfect for learning, but production devices need encryption and their own credentials. With a managed MQTT broker from ConnectMQ you create a broker in the portal and receive a generated username, password and client ID. The change to the sketch is small:

#include <WiFiClientSecure.h>

WiFiClientSecure netClient;   // replaces WiFiClient
PubSubClient mqtt(netClient);

// ISRG Root X1, the Let's Encrypt root used by broker.connectmq.com
const char* ROOT_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste ISRG Root X1 PEM here...
-----END CERTIFICATE-----
)EOF";

void setup() {
  // ... Wi-Fi as before ...
  netClient.setCACert(ROOT_CA);
  mqtt.setServer("broker.connectmq.com", 8883);
}

bool connectMqtt() {
  return mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD",
                      topicStatus, 1, true, "offline");
}

ConnectMQ binds each client ID to the credentials it issued, so use the exact client ID from the portal. Topic prefixes on a private broker are yours to design; the public connectmq/demo/... prefix is no longer needed. For more hardening ideas, see MQTT security best practices.

Troubleshooting Checklist

  1. Connects, then drops every few seconds: two devices share a client ID. The broker disconnects the older session whenever the second one connects.
  2. publish() returns false: payload plus topic exceeds the buffer, or the client is not connected.
  3. Callback never fires: you forgot mqtt.loop(), the subscription was made before the connection existed, or the message is larger than the buffer.
  4. Random disconnects under load: long blocking code in loop(); break it up or move it to a timer.
  5. Works on 1883, fails on 8883: wrong root certificate, or the clock is not set on boards that check validity dates (common on ESP8266).

If you are choosing between libraries, the ArduinoMqttClient library guide and the async MQTT client for ESP32 cover the main alternatives, and the ESP32 IoT tutorial puts all the pieces together into a complete sensor project.

Connect Your PubSubClient Devices to ConnectMQ

Once your sketch runs against the public broker, moving it to a private cloud MQTT broker is a two-line change. ConnectMQ gives you MQTT over TCP, TLS on 8883 and WebSockets, a browser MQTT client for testing, a live topic explorer and threshold alert rules. Create a free Serverless broker at app.connectmq.com, no credit card required, and you will have credentials in a couple of minutes.

Frequently Asked Questions

Does PubSubClient support MQTT 5?

No. PubSubClient speaks MQTT 3.1.1, which every mainstream broker, including ConnectMQ, still supports alongside MQTT 5. If you need MQTT 5 properties on the device, choose a different client library.

Can PubSubClient publish with QoS 1?

No. publish() always sends QoS 0. You can subscribe with QoS 1, so commands sent to the device with QoS 1 are delivered reliably by the broker.

Why does my PubSubClient payload print strange characters?

The payload buffer is not null-terminated. Copy length bytes into your own buffer and add a terminating \0 before treating it as a string.

What is the maximum message size in PubSubClient?

The buffer size is a 16-bit value, so the practical ceiling is just under 64 KB, but RAM is the real limit. Keep messages small on constrained boards and remember that your broker plan also has a maximum message size.

Related articles

ESP-IDF MQTT Example: Build an esp-mqtt Client from Scratch

Build an ESP-IDF project from an empty folder with the esp-mqtt component: Wi-Fi station setup, the MQTT event handler, publish and subscribe, MQTT 5 properties and TLS with the certificate bundle.

8 min read

Using esp-mqtt MQTT 5 Features on ESP32 with ESP-IDF

A hands-on ESP-IDF project that enables MQTT 5.0 in esp-mqtt and uses user properties, message expiry, topic aliases, session expiry and response topics against a real MQTT 5 broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.