How to Use MQTT with Zig on ESP32: A Complete Guide to ESP-IDF Integration

Use the idf.mqtt module from the kassane/zig-esp-idf-sample repository to wrap the ESP-IDF C API, configure a ClientConfig, initialize with mqtt.init(), and manage connections via mqtt.start() and mqtt.stop().

The kassane/zig-esp-idf-sample project provides a lightweight Zig wrapper around the ESP-IDF MQTT client, allowing you to use MQTT with Zig on ESP32 devices without writing C bindings manually. This wrapper translates C error codes into Zig errors and exposes a type-safe API for configuring brokers, publishing messages, and handling events. All MQTT functionality resides in imports/mqtt.zig and is re-exported through imports/idf.zig as idf.mqtt.

Setting Up MQTT in Your Zig ESP32 Project

Importing the MQTT Module

To begin using MQTT with Zig on ESP32, import the module from the ESP-IDF namespace. The idf.zig file re-exports the MQTT wrapper, making it accessible as idf.mqtt:

const idf = @import("esp_idf");
const mqtt = idf.mqtt;
const log = @import("log");

This import gives you access to all core types including mqtt.Handle, mqtt.ClientConfig, and error sets defined in imports/mqtt.zig.

Configuring the Client

Before connecting, populate a mqtt.ClientConfig struct. This struct mirrors the ESP-IDF esp_mqtt_client_config_t and is defined at line 12 of imports/mqtt.zig. At minimum, you must specify the broker URI:

var config = mqtt.ClientConfig{
    .uri = "mqtt://test.mosquitto.org",
    .client_id = "zig_esp32_client",
    .keepalive = 60,
    .disable_auto_reconnect = false,
    .disable_clean_session = false,
    .username = null,
    .password = null,
    .event_handle = null,
};

Key configuration fields include:

  • .uri: The broker address (e.g., mqtt://broker.hivemq.com or mqtts://secure.broker:8883).
  • .keepalive: Keep-alive interval in seconds.
  • .disable_auto_reconnect: Set to true to disable automatic reconnection logic.

Initializing and Starting the MQTT Client

Once configured, initialize the client using mqtt.init() defined at lines 58-60 in imports/mqtt.zig. This function returns an optional Handle (opaque pointer to the underlying C client) or null on allocation failure:

const client = mqtt.init(&config) orelse {
    log.err("Failed to allocate MQTT client", .{});
    return;
};

After initialization, start the connection process with mqtt.start() (lines 77-79 in imports/mqtt.zig). This function initiates the underlying network connection and MQTT handshake, returning a Zig error if the ESP-IDF call fails:

mqtt.start(client) catch |e| {
    log.err("MQTT start failed: {}", .{e});
    return;
};

To gracefully disconnect, call mqtt.stop(client) followed by mqtt.destroy(client) to free resources.

Publishing and Subscribing to Topics

Publishing Messages

Use mqtt.publish() (lines 97-112 in imports/mqtt.zig) to send messages immediately. The function signature accepts the client handle, topic string, payload bytes, and a configuration struct for QoS and retain flags:

const topic = "zig/esp32/sensor";
const payload = "temperature=23.5";

const msg_id = mqtt.publish(
    client,
    topic,
    payload,
    .{
        .qos = 1,      // At least once delivery
        .retain = false,
    }
) catch |e| {
    log.err("Publish failed: {}", .{e});
    return 0;
};

log.info("Published message with ID: {}", .{msg_id});

The function returns a u32 message ID on success, which corresponds to the packet identifier assigned by the ESP-IDF client.

Subscribing to Topics

Subscribe using mqtt.subscribe() (lines 38-45 in imports/mqtt.zig). This function takes the client handle, topic filter, and QoS level:

mqtt.subscribe(client, "zig/esp32/commands", 1) catch |e| {
    log.err("Subscribe failed: {}", .{e});
};

For multiple subscriptions in a single call, use mqtt.subscribeMultiple() (lines 46-50 in imports/mqtt.zig), which accepts a slice of topic/QoS pairs.

Handling Offline Messages with Enqueue

When the client is disconnected, mqtt.enqueue() (lines 14-31 in imports/mqtt.zig) stores messages in the ESP-IDF outbox for later transmission. This is useful for maintaining telemetry during network interruptions:

mqtt.enqueue(
    client,
    "zig/esp32/offline",
    "cached_data",
    .{
        .qos = 1,
        .retain = false,
        .store = true,  // Store in outbox when offline
    }
) catch |e| {
    log.err("Enqueue failed: {}", .{e});
};

Handling MQTT Events in Zig

The ESP-IDF MQTT client operates asynchronously and communicates via an event loop. Register callbacks using mqtt.registerEvent() (lines 60-66 in imports/mqtt.zig) to handle connection status, incoming messages, and errors:

fn mqttEventHandler(event: *mqtt.Event, arg: ?*anyopaque) callconv(.C) void {
    _ = arg; // Unused user argument
    
    switch (event.event_id) {
        .MQTT_EVENT_CONNECTED => {
            log.info("Connected to broker", .{});
        },
        .MQTT_EVENT_DISCONNECTED => {
            log.warn("Disconnected from broker", .{});
        },
        .MQTT_EVENT_DATA => {
            const topic = event.topic[0..event.topic_len];
            const data = event.data[0..event.data_len];
            log.info("Received on {s}: {s}", .{ topic, data });
        },
        .MQTT_EVENT_ERROR => {
            log.err("MQTT error occurred", .{});
        },
        else => {},
    }
}

// In app_main:
mqtt.registerEvent(client, .MQTT_EVENT_ANY, mqttEventHandler, null) catch |e| {
    log.err("Failed to register event handler: {}", .{e});
};

The event handler uses C calling convention (callconv(.C)) because it interfaces directly with the ESP-IDF C layer. The mqtt.Event struct provides fields for topic, data, message ID, and error codes.

Advanced MQTT Features

MQTT 5 Support

The wrapper includes experimental support for MQTT protocol version 5. Access these features through the mqtt.v5 submodule, which provides functions like setConnectProperty() and setUserProperty() (lines 186-236 in imports/mqtt.zig). These allow setting properties such as session expiry intervals and user-defined key-value pairs in CONNECT packets.

Custom Events and Configuration Options

For complex applications, use mqtt.dispatchCustomEvent() to inject user-defined events into the MQTT event loop, enabling coordination between your application logic and the MQTT client thread. Additionally, the ClientConfig struct supports TLS configuration via the .cert_pem, .client_cert_pem, and .client_key_pem fields for secure broker connections.

Summary

  • Import path: Access MQTT functionality via const mqtt = @import("esp_idf").mqtt; from imports/idf.zig.
  • Configuration: Define broker settings using mqtt.ClientConfig in imports/mqtt.zig (line 12), ensuring you set at least the .uri field.
  • Lifecycle: Initialize with mqtt.init() (line 58), connect with mqtt.start() (line 77), and clean up with mqtt.stop() and mqtt.destroy().
  • Messaging: Publish immediately with mqtt.publish() (line 97), subscribe with mqtt.subscribe() (line 38), and queue offline messages via mqtt.enqueue() (line 14).
  • Events: Register asynchronous handlers using mqtt.registerEvent() (line 60) to process incoming data and connection state changes.

Frequently Asked Questions

How do I configure the MQTT broker URI in Zig?

Set the .uri field of mqtt.ClientConfig to a valid MQTT broker address before calling mqtt.init(). For example, use mqtt://broker.hivemq.com for unencrypted connections or mqtts://secure.broker:8883 for TLS-encrypted brokers. This configuration is defined at line 12 of imports/mqtt.zig.

What is the difference between mqtt.publish and mqtt.enqueue?

mqtt.publish() (lines 97-112 in imports/mqtt.zig) sends messages immediately and requires an active connection, returning a message ID or an error if the client is disconnected. In contrast, mqtt.enqueue() (lines 14-31) stores messages in the ESP-IDF outbox when the client is offline, transmitting them automatically once connectivity is restored.

How do I handle incoming MQTT messages in Zig?

Register an event handler using mqtt.registerEvent() (line 60 in imports/mqtt.zig), specifying .MQTT_EVENT_DATA or .MQTT_EVENT_ANY as the event type. Your handler function must use callconv(.C) and accept *mqtt.Event and ?*anyopaque parameters. Inside the handler, check event.event_id for .MQTT_EVENT_DATA and access event.topic and event.data to process incoming payloads.

Does this Zig wrapper support MQTT 5?

Yes, the wrapper includes experimental MQTT 5 support accessible through the mqtt.v5 submodule defined in imports/mqtt.zig (lines 186-236). This submodule provides functions like setConnectProperty() and setUserProperty() to configure MQTT 5 specific features such as session expiry intervals and user-defined properties in CONNECT packets.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →