An async MQTT client on the ESP32 lets your firmware keep reading sensors, driving displays and serving web pages while MQTT traffic is handled in the background. In this guide you will build a complete AsyncMqttClient sketch with event callbacks, automatic reconnects and QoS 1 publishing against a real broker. You will also learn the library's pitfalls so you can decide when it is the right choice.
Why Use an Async MQTT Client
Blocking libraries such as PubSubClient work well, but they rely on you calling loop() frequently, and connect() can freeze your sketch for up to the socket timeout when the network is bad. On a device that also drives a motor, refreshes an LED strip or hosts a configuration page, those pauses are visible.
AsyncMqttClient, by Marvin Roger, takes a different approach. It sits on top of the AsyncTCP library, which runs networking in its own FreeRTOS task. Your code registers callbacks for events (connected, disconnected, message received, publish acknowledged) and the library calls them when something happens. There is no loop() to call.
| Aspect | PubSubClient | AsyncMqttClient |
|---|---|---|
| Programming model | Polling, call loop() |
Event callbacks |
connect() behaviour |
Blocks until CONNACK or timeout | Returns immediately |
| Publish QoS | 0 | 0, 1 and 2 |
| Subscribe QoS | 0 and 1 | 0, 1 and 2 |
| Large incoming payloads | Must fit buffer | Delivered in chunks |
| TLS on ESP32 | Yes, via WiFiClientSecure |
Not practical with standard AsyncTCP |
| Dependencies | None | AsyncTCP |
That last row matters. AsyncMqttClient's TLS support depends on an SSL-enabled build of AsyncTCP, which is not available in the standard ESP32 AsyncTCP library. In practice this library is best for plain MQTT on port 1883 inside a trusted network, or for prototyping. If you need TLS with an event-driven API, the actively maintained espMqttClient library offers a very similar callback style with TLS support on ESP32. We cover the trade-offs at the end.
Install the Libraries
- Install the "esp32 by Espressif Systems" board package in the Arduino IDE.
- Install AsyncTCP for ESP32. Several maintained forks exist in the Library Manager; pick one that matches your ESP32 core version.
- Install AsyncMqttClient. If it does not appear in the Library Manager, download the release ZIP from the project repository and use Sketch → Include Library → Add .ZIP Library.
- With PlatformIO, add both to
lib_depsinplatformio.ini.
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
marvinroger/AsyncMqttClient
esp32async/AsyncTCP
The Complete Async MQTT Client ESP32 Sketch
This example connects to broker.connectmq.com, the public MQTT test broker from ConnectMQ, on port 1883. It reconnects Wi-Fi and MQTT with FreeRTOS timers, publishes telemetry at QoS 1 every ten seconds, and toggles the onboard LED when a command arrives.
#include <WiFi.h>
#include <AsyncMqttClient.h>
extern "C" {
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
}
#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD"
#define MQTT_HOST "broker.connectmq.com"
#define MQTT_PORT 1883
#define LED_PIN 2
static const char* DEVICE_ID = "async-4b81d2"; // make this unique
String topicTelemetry = String("connectmq/demo/") + DEVICE_ID + "/telemetry";
String topicCommand = String("connectmq/demo/") + DEVICE_ID + "/led/set";
String topicStatus = String("connectmq/demo/") + DEVICE_ID + "/status";
AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;
unsigned long lastPublish = 0;
void connectToWifi() {
Serial.println("Connecting to Wi-Fi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
void connectToMqtt() {
Serial.println("Connecting to MQTT...");
mqttClient.connect();
}
void onWifiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
Serial.print("Wi-Fi connected, IP ");
Serial.println(WiFi.localIP());
connectToMqtt();
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
Serial.println("Wi-Fi lost");
xTimerStop(mqttReconnectTimer, 0); // no MQTT while Wi-Fi is down
xTimerStart(wifiReconnectTimer, 0);
break;
default:
break;
}
}
void onMqttConnect(bool sessionPresent) {
Serial.printf("MQTT connected, session present: %d\n", sessionPresent);
mqttClient.subscribe(topicCommand.c_str(), 1);
mqttClient.publish(topicStatus.c_str(), 1, true, "online");
}
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
Serial.printf("MQTT disconnected, reason %d\n", static_cast<int>(reason));
if (WiFi.isConnected()) {
xTimerStart(mqttReconnectTimer, 0);
}
}
void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
Serial.printf("Subscribe ack, packetId %u, granted QoS %u\n", packetId, qos);
}
void onMqttPublish(uint16_t packetId) {
Serial.printf("Publish ack (PUBACK), packetId %u\n", packetId);
}
void onMqttMessage(char* topic, char* payload,
AsyncMqttClientMessageProperties properties,
size_t len, size_t index, size_t total) {
// payload is not null-terminated and may be one chunk of a larger message
if (index != 0 || len != total) {
Serial.println("Fragmented message ignored in this demo");
return;
}
String msg;
msg.reserve(len);
for (size_t i = 0; i < len; i++) msg += payload[i];
Serial.printf("Message on %s (QoS %u): %s\n", topic, properties.qos, msg.c_str());
if (msg == "ON") digitalWrite(LED_PIN, HIGH);
if (msg == "OFF") digitalWrite(LED_PIN, LOW);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE,
nullptr, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE,
nullptr, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));
WiFi.onEvent(onWifiEvent);
mqttClient.onConnect(onMqttConnect);
mqttClient.onDisconnect(onMqttDisconnect);
mqttClient.onSubscribe(onMqttSubscribe);
mqttClient.onPublish(onMqttPublish);
mqttClient.onMessage(onMqttMessage);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
mqttClient.setClientId(DEVICE_ID); // pointer must stay valid
mqttClient.setKeepAlive(30);
mqttClient.setWill(topicStatus.c_str(), 1, true, "offline");
connectToWifi();
}
void loop() {
// Your application code runs freely here; MQTT is handled in the background.
if (mqttClient.connected() && millis() - lastPublish > 10000) {
lastPublish = millis();
char payload[96];
snprintf(payload, sizeof(payload), "{\"uptime\":%lu,\"rssi\":%d,\"heap\":%u}",
millis() / 1000, WiFi.RSSI(), (unsigned)ESP.getFreeHeap());
uint16_t id = mqttClient.publish(topicTelemetry.c_str(), 1, false, payload);
Serial.printf("Queued publish, packetId %u\n", id);
}
}
Watch the traffic and send commands from your computer:
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-4b81d2 -t "connectmq/demo/async-4b81d2/#" -v -q 1
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-4b81d2 -t "connectmq/demo/async-4b81d2/led/set" -m ON -q 1
How the Event Flow Works
Understanding the order of events prevents most bugs:
connectToWifi()starts the Wi-Fi connection and returns at once.- When the station gets an IP address,
onWifiEventcallsconnectToMqtt(). mqttClient.connect()returns immediately. Later, AsyncTCP's task receives the CONNACK and the library callsonMqttConnect.- Inside
onMqttConnectyou subscribe and publish your "online" status. This is the right place, because subscriptions on a clean session must be renewed after every reconnect. - If the broker or network drops you,
onMqttDisconnectschedules a one-shot timer, which callsconnectToMqtt()two seconds later.
publish() returns a packet ID for QoS 1 and 2 messages, 1 for a QoS 0 message that was queued, and 0 when the message could not be sent. The matching onMqttPublish(packetId) fires when the broker acknowledges delivery, which lets you track whether important messages actually arrived.
Pitfalls That Bite in Production
Callbacks Run in Another Task
Your callbacks execute in the AsyncTCP task, not in loop(). Do not call delay() or perform slow I/O there; doing so blocks all network traffic and can trigger the task watchdog. Copy data into a variable or queue and handle it in loop(). If loop() and a callback share a variable, protect it, for example with a FreeRTOS queue or a volatile flag for simple cases.
String Lifetimes
setClientId(), setCredentials(), setServer() with a host name and setWill() store pointers, not copies. Passing the c_str() of a temporary String leads to garbage being sent on reconnect. Use global constants or global String objects, as the example does.
Fragmented Payloads
Large incoming messages arrive in several onMessage calls with increasing index and the same total. If you expect payloads of more than a few hundred bytes, accumulate chunks into a buffer of size total and process when index + len == total.
No Built-In Reconnect
The library does not reconnect by itself. The timer pattern above is the standard approach. Always stop the MQTT timer when Wi-Fi drops, otherwise you spam connect attempts that cannot succeed.
Persistent Sessions for Missed Commands
By default the client connects with a clean session, so commands sent while the device was offline are lost. Call mqttClient.setCleanSession(false) before connecting and subscribe with QoS 1: the broker then keeps the session and queues QoS 1 messages for the device until it returns. The session does not live forever; brokers expire it after a while (on the public broker, sessions expire after at most 30 minutes). Check sessionPresent in onMqttConnect to see whether the broker remembered you, and resubscribe when it did not.
Broker Limits Still Apply
Async does not mean unlimited. The public broker allows 10 messages per second per client, 10 subscriptions per client and 64 KB per message. A tight loop calling publish() will hit those limits quickly.
Choosing Between Async and Blocking Clients
Use this checklist to decide:
- Need TLS on port 8883? Choose PubSubClient with
WiFiClientSecure, or espMqttClient's secure client. Standard AsyncMqttClient is not a good fit. - Need QoS 1 or 2 publishing from the device? AsyncMqttClient or espMqttClient.
- Running a web server (ESPAsyncWebServer) on the same device? AsyncMqttClient shares the AsyncTCP stack nicely.
- Targeting ESP8266 as well? AsyncMqttClient works there with ESPAsyncTCP, though memory is tighter. See ESP8266 MQTT tutorial.
- Want the simplest code for a beginner project? The PubSubClient tutorial is the gentler start.
If your messages are about delivery guarantees, read MQTT QoS levels explained before switching libraries just for QoS 2; many sensor workloads are best served by QoS 0 or 1. The broader ESP32 IoT tutorial shows how MQTT fits into a full sensor project.
Production Setup with ConnectMQ
For deployments outside your lab, run the device against a dedicated MQTT broker in the cloud rather than the public area. On ConnectMQ, you create a broker in the portal and receive a username, password and client ID. With AsyncMqttClient you set them like this, keeping in mind that client IDs are bound to the issued credentials:
mqttClient.setServer("broker.connectmq.com", 1883);
mqttClient.setClientId("YOUR_CLIENT_ID");
mqttClient.setCredentials("YOUR_USERNAME", "YOUR_PASSWORD");
Because credentials on port 1883 travel unencrypted, prefer a library with TLS support for devices outside a trusted network.
Get Started with ConnectMQ
ConnectMQ is a hosted MQTT broker with MQTT 3.1.1 and 5.0, TLS, WebSockets, a live topic explorer and threshold alerts in the portal, so you can watch your ESP32 publish in real time. Create a free Serverless broker at app.connectmq.com with no credit card and connect your first async device in minutes.
Frequently Asked Questions
Does AsyncMqttClient work with TLS on the ESP32?
Not with the standard AsyncTCP library, which has no SSL build for ESP32. Use PubSubClient with WiFiClientSecure or a maintained async library such as espMqttClient when you need port 8883.
Why do I need FreeRTOS timers for reconnecting?
AsyncMqttClient has no automatic reconnect. One-shot timers let you retry after a short delay without blocking loop() or the network task.
Can I call delay() inside onMqttMessage?
No. Callbacks run in the AsyncTCP task, and blocking there stalls all network traffic. Store the message and process it in loop().
Is AsyncMqttClient still maintained?
The original library has not seen active development for some time, though it remains widely used. Check the repository status before starting a new long-lived product and consider espMqttClient as a maintained alternative.



