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

> Learn to expertly use MQTT with Zig on ESP32. This guide details ESP-IDF integration, client configuration, and connection management using the zig-esp-idf-sample repository.

- Repository: [Matheus C. França/zig-esp-idf-sample](https://github.com/kassane/zig-esp-idf-sample)
- Tags: how-to-guide
- Published: 2026-03-05

---

**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`:

```zig
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:

```zig
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:

```zig
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:

```zig
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:

```zig
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:

```zig
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:

```zig
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:

```zig
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.