Tutorials

MQTT with ESP-IDF (esp-mqtt) on ESP32: Config, Events and TLS

Use the ESP-IDF esp-mqtt client on ESP32: esp_mqtt_client_config_t, the event handler, publish and subscribe, Last Will, TLS on 8883 and how to test it.

Updated · 9 min read

MQTT with ESP-IDF (esp-mqtt) on ESP32: Config, Events and TLS

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:

Create the project
idf.py create-project mqtt_demo
cd mqtt_demo
idf.py set-target esp32

Make 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.

CMakeLists.txt (project root)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
main/CMakeLists.txt
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:

URITransportPort
mqtt://broker.connectmq.com:1883TCP1883
mqtts://broker.connectmq.com:8883TLS8883
ws://broker.connectmq.com:8083/mqttWebSocket8083
wss://broker.connectmq.com:8084/mqttSecure WebSocket8084

A realistic configuration also sets a client ID, credentials, keep alive and a Last Will:

main/main.c — configuration
#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:

main/main.c — event handler
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 after start(). The client may reconnect many times; subscribing on every connect restores your subscriptions even when the broker did not keep the session.
  • topic and data are not C strings. Always use the _len fields. Large messages arrive in several MQTT_EVENT_DATA chunks; event->current_data_offset and event->total_data_len let 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:

main/main.c — 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:

Telemetry 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:

  1. testmqtt/espidf/dev01/status = online, flagged as retained.
  2. 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):

TLS with the 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:

main/CMakeLists.txt
idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
                       PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common
                       EMBED_TXTFILES "ca.pem")
Embedded CA certificate
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 lineCauseFix
esp-tls: couldn't get hostname for :broker.connectmq.com:DNS failed, usually Wi-Fi not connected yetStart MQTT only after you have an IP
mbedtls_ssl_handshake returned -0x2700Certificate verification failedUse the bundle or the right CA; check the system time
mbedtls_ssl_setup returned -0x7F00Out of memory for TLSFree heap, lower other buffers
Error transport connectPort blocked or wrong scheme/port pairCheck mqtts:// with 8883, mqtt:// with 1883
MQTT_ERROR_TYPE_CONNECTION_REFUSED, code 5Not authorizedCheck 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.