esp-mqtt is the MQTT client that comes with Espressif's ESP-IDF framework. It runs in its own FreeRTOS task, reconnects automatically, supports MQTT 3.1.1 and MQTT 5, and speaks TCP, TLS, WebSocket and secure WebSocket through a single URI. You configure it with one esp_mqtt_client_config_t struct, register an event handler and call esp_mqtt_client_start().
This tutorial builds a complete ESP32 application step by step: plain TCP first, then events, publishing, subscriptions, a Last Will, and finally TLS on port 8883. At each stage you verify the result from a browser so you know the device, not your test, is the problem when something breaks.
Project setup
You need ESP-IDF 5.x installed (idf.py --version should work in your terminal). Start from the Wi-Fi helper used by the official examples so the tutorial can focus on MQTT:
idf.py create-project mqtt_demo
cd mqtt_demo
idf.py set-target esp32Make the example Wi-Fi component visible in the top-level CMakeLists.txt (before the include(...project.cmake) line) and declare the dependencies of main. The mqtt component is part of ESP-IDF, so #include "mqtt_client.h" works without extra downloads on 5.x releases.
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common)Then run idf.py menuconfig, open Example Connection Configuration and enter your Wi-Fi SSID and password.
The client configuration
The minimal configuration is a broker URI. The scheme selects the transport:
| URI | Transport | Port |
|---|---|---|
mqtt://broker.connectmq.com:1883 | TCP | 1883 |
mqtts://broker.connectmq.com:8883 | TLS | 8883 |
ws://broker.connectmq.com:8083/mqtt | WebSocket | 8083 |
wss://broker.connectmq.com:8084/mqtt | Secure WebSocket | 8084 |
A realistic configuration also sets a client ID, credentials, keep alive and a Last Will:
#include "esp_log.h"
#include "mqtt_client.h"
static const char *TAG = "mqtt_demo";
static esp_mqtt_client_handle_t client;
#define BASE_TOPIC "testmqtt/espidf/dev01"
static esp_mqtt_client_handle_t mqtt_create(void)
{
const esp_mqtt_client_config_t cfg = {
.broker.address.uri = "mqtt://broker.connectmq.com:1883",
.credentials.client_id = "espidf-dev01",
.credentials.username = "username",
.credentials.authentication.password = "password",
.session.keepalive = 30,
.session.last_will = {
.topic = BASE_TOPIC "/status",
.msg = "offline",
.qos = 1,
.retain = 1,
},
.network.reconnect_timeout_ms = 5000,
};
return esp_mqtt_client_init(&cfg);
}Every client needs a unique client ID: two devices with the same ID kick each other off the broker in an endless loop. Derive it from the MAC address in production, or generate one with the client ID generator while testing. Keep credentials out of source control by moving them into Kconfig.projbuild options (CONFIG_MQTT_USER, CONFIG_MQTT_PASS) and setting them in menuconfig or sdkconfig.defaults.
Handling events
esp-mqtt reports everything through the ESP event loop. One handler receives all events; event_data points to an esp_mqtt_event_t:
static void mqtt_event_handler(void *args, esp_event_base_t base,
int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t event = event_data;
switch ((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "connected");
esp_mqtt_client_publish(client, BASE_TOPIC "/status", "online", 0, 1, 1);
esp_mqtt_client_subscribe(client, BASE_TOPIC "/cmd/#", 1);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "disconnected, will retry");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "subscribed, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "broker acked msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
// topic and data are NOT null-terminated
ESP_LOGI(TAG, "%.*s = %.*s", event->topic_len, event->topic,
event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
ESP_LOGE(TAG, "transport error: esp-tls 0x%x, tls stack 0x%x, errno %d",
event->error_handle->esp_tls_last_esp_err,
event->error_handle->esp_tls_stack_err,
event->error_handle->esp_transport_sock_errno);
} else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
ESP_LOGE(TAG, "refused, CONNACK code %d",
event->error_handle->connect_return_code);
}
break;
default:
break;
}
}Three details trip up almost everyone:
- Subscribe in
MQTT_EVENT_CONNECTED, not afterstart(). The client may reconnect many times; subscribing on every connect restores your subscriptions even when the broker did not keep the session. topicanddataare not C strings. Always use the_lenfields. Large messages arrive in severalMQTT_EVENT_DATAchunks;event->current_data_offsetandevent->total_data_lenlet you reassemble them, or raise the buffer size with.buffer.size.- The handler runs in the MQTT task. Do not block in it. Hand long work to another task through a FreeRTOS queue.
Start everything from app_main:
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "protocol_examples_common.h"
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect()); // blocks until Wi-Fi has an IP
client = mqtt_create();
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_mqtt_client_start(client);
}Flash and watch the log with idf.py flash monitor. You should see connected followed by subscribed.
Publishing telemetry
esp_mqtt_client_publish(client, topic, data, len, qos, retain) returns a message ID (0 for QoS 0, -1 on error). Pass len = 0 to let the library call strlen() on a string payload. For QoS 1 and 2 the call writes from the calling task and may wait on the network; esp_mqtt_client_enqueue() takes the same arguments plus a store flag and leaves the sending to the MQTT task:
static void telemetry_task(void *arg)
{
char payload[64];
for (int i = 0;; i++) {
int len = snprintf(payload, sizeof payload,
"{\"seq\":%d,\"heap\":%lu}", i,
(unsigned long)esp_get_free_heap_size());
int id = esp_mqtt_client_enqueue(client, BASE_TOPIC "/telemetry",
payload, len, 1, 0, true);
if (id < 0) ESP_LOGW(TAG, "outbox full or not connected");
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
// in app_main, after esp_mqtt_client_start():
// xTaskCreate(telemetry_task, "telemetry", 3072, NULL, 5, NULL);Messages queued while the client is offline stay in the outbox (bounded by CONFIG_MQTT_OUTBOX_EXPIRED_TIMEOUT_MS and available heap) and are sent after reconnecting. Choose the QoS deliberately: see MQTT QoS levels for what each level costs.
Verify from the browser
Open the online MQTT client, connect to broker.connectmq.com (WebSocket 8084, path /mqtt) and subscribe to testmqtt/espidf/dev01/#. You should see:
testmqtt/espidf/dev01/status=online, flagged as retained.- A telemetry message every five seconds.
Now publish reboot to testmqtt/espidf/dev01/cmd/system from the browser: the ESP32 log prints the topic and payload. Finally pull the power cable. After about 1.5 × keep alive (45 seconds here) the broker publishes the Last Will and the status flips to offline. That one test proves connection, publish, subscribe and the will all work. How the Last Will works explains the timing.
Watch your ESP32 in the online client
Adding TLS on port 8883
Switching to TLS is two changes: an mqtts:// URI and a way to verify the server certificate. The simplest is the ESP x509 certificate bundle, which contains the common public root CAs and is enabled by default (CONFIG_MBEDTLS_CERTIFICATE_BUNDLE):
#include "esp_crt_bundle.h"
const esp_mqtt_client_config_t cfg = {
.broker.address.uri = "mqtts://broker.connectmq.com:8883",
.broker.verification.crt_bundle_attach = esp_crt_bundle_attach,
.credentials.client_id = "espidf-dev01",
.credentials.username = "username",
.credentials.authentication.password = "password",
};If your broker uses a private CA or you want to pin a specific root, embed the PEM file instead:
idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common
EMBED_TXTFILES "ca.pem")extern const uint8_t ca_pem_start[] asm("_binary_ca_pem_start");
const esp_mqtt_client_config_t cfg = {
.broker.address.uri = "mqtts://broker.connectmq.com:8883",
.broker.verification.certificate = (const char *)ca_pem_start,
};TLS needs the correct time to check certificate validity and roughly 30–40 KB of extra heap during the handshake; on a busy ESP32 that is the first place to look when connections fail. Our ESP32 TLS guide covers client certificates (.credentials.authentication.certificate and .key) for mutual TLS.
Common esp-mqtt errors
| Log line | Cause | Fix |
|---|---|---|
esp-tls: couldn't get hostname for :broker.connectmq.com: | DNS failed, usually Wi-Fi not connected yet | Start MQTT only after you have an IP |
mbedtls_ssl_handshake returned -0x2700 | Certificate verification failed | Use the bundle or the right CA; check the system time |
mbedtls_ssl_setup returned -0x7F00 | Out of memory for TLS | Free heap, lower other buffers |
Error transport connect | Port blocked or wrong scheme/port pair | Check mqtts:// with 8883, mqtt:// with 1883 |
MQTT_ERROR_TYPE_CONNECTION_REFUSED, code 5 | Not authorized | Check username, password and ACLs |
The numeric CONNACK codes and MQTT 5 reason codes are listed in the reason code lookup. For devices that connect and then drop every few minutes, read ESP32 Wi-Fi and MQTT disconnects.
Enabling MQTT 5
Turn on CONFIG_MQTT_PROTOCOL_5 in menuconfig (Component config → ESP-MQTT Configurations) and set .session.protocol_ver = MQTT_PROTOCOL_V_5. The basic publish and subscribe calls stay the same; MQTT 5 extras such as user properties, message expiry and response topics are set through functions like esp_mqtt5_client_set_publish_property() before the publish call. Refused connections then report precise reason codes instead of the six MQTT 3.1.1 return codes, which makes debugging authentication much easier.
esp-mqtt gives you a production-grade client with very little code: one config struct, one event handler, and the transport chosen by the URI. Get it working on plain TCP, prove publish, subscribe and the will from the online client, then switch to mqtts:// and keep the same tests as your regression check before every firmware release.
Frequently asked questions
Do I need an extra library for MQTT in ESP-IDF?
No for most projects. The esp-mqtt client ships with ESP-IDF (header mqtt_client.h). Some newer ESP-IDF releases distribute it through the component registry as espressif/mqtt, so check the documentation for your version if the header is not found.
Why does esp_mqtt_client_publish block?
For QoS 1 and 2 it writes to the socket from the calling task and can wait on the network. Use esp_mqtt_client_enqueue to put the message in the outbox and let the MQTT task send it, which is safer from time-critical code.
Does esp-mqtt support MQTT 5?
Yes. Enable MQTT 5 in menuconfig (Component config → ESP-MQTT Configurations) and set session.protocol_ver to MQTT_PROTOCOL_V_5. You then get reason codes, user properties and the other MQTT 5 extras through dedicated APIs.

