ESP32 and Arduino

Async MQTT Client on ESP32: Non-Blocking MQTT with AsyncMqttClient

Learn how AsyncMqttClient keeps ESP32 firmware responsive: event-driven callbacks, automatic reconnects with FreeRTOS timers, QoS 1 and 2 publishing and fragmented payload handling.

ESP32 board with event arrows illustrating an async MQTT client ESP32 setup sending messages without blocking
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

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

  1. Install the "esp32 by Espressif Systems" board package in the Arduino IDE.
  2. Install AsyncTCP for ESP32. Several maintained forks exist in the Library Manager; pick one that matches your ESP32 core version.
  3. 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.
  4. With PlatformIO, add both to lib_deps in platformio.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:

  1. connectToWifi() starts the Wi-Fi connection and returns at once.
  2. When the station gets an IP address, onWifiEvent calls connectToMqtt().
  3. mqttClient.connect() returns immediately. Later, AsyncTCP's task receives the CONNACK and the library calls onMqttConnect.
  4. Inside onMqttConnect you subscribe and publish your "online" status. This is the right place, because subscriptions on a clean session must be renewed after every reconnect.
  5. If the broker or network drops you, onMqttDisconnect schedules a one-shot timer, which calls connectToMqtt() 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:

  1. Need TLS on port 8883? Choose PubSubClient with WiFiClientSecure, or espMqttClient's secure client. Standard AsyncMqttClient is not a good fit.
  2. Need QoS 1 or 2 publishing from the device? AsyncMqttClient or espMqttClient.
  3. Running a web server (ESPAsyncWebServer) on the same device? AsyncMqttClient shares the AsyncTCP stack nicely.
  4. Targeting ESP8266 as well? AsyncMqttClient works there with ESPAsyncTCP, though memory is tighter. See ESP8266 MQTT tutorial.
  5. 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.

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.