---
title: "Zig e MQTT: Cliente IoT e Pub/Sub para Sistemas Embarcados"
url: "https://ziglang.com.br/artigos/zig-mqtt-cliente-iot/"
markdown_url: "https://ziglang.com.br/artigos/zig-mqtt-cliente-iot.MD"
description: "Como usar MQTT com Zig: publicar e assinar em um broker (Mosquitto), QoS, keepalive, last will e TLS. Cliente IoT leve e nativo para sistemas embarcados."
date: "2026-06-27"
author: ""
---

# Zig e MQTT: Cliente IoT e Pub/Sub para Sistemas Embarcados

Como usar MQTT com Zig: publicar e assinar em um broker (Mosquitto), QoS, keepalive, last will e TLS. Cliente IoT leve e nativo para sistemas embarcados.


Se você já publicou um tópico MQTT a partir de um microcontrolador, provavelmente usou uma biblioteca em C herdada, um wrapper de vendor ou um stub gerado para o seu RTOS. O que poucos percebem é que **o Zig é uma das linguagens mais confortáveis para falar MQTT hoje**: o protocolo é binário, compacto e previsível, e o Zig foi desenhado justamente para ler e montar bytes assim, sem alocação escondida e sem runtime. O resultado é um cliente MQTT que cabe em poucos quilobytes, compila para o seu alvo embarcado e você entende por inteiro.

Este guia mostra como usar MQTT com Zig de forma prática: entender o modelo pub/sub, escolher entre usar uma biblioteca C (como a libmosquitto) e escrever um cliente nativo em Zig, montar os pacotes `CONNECT`, `PUBLISH` e `SUBSCRIBE` na mão, garantir QoS, keepalive e last will, fechar a conexão com TLS e operar em produção. Ele complementa o material sobre [IoT e embarcados em Zig](/artigos/zig-embarcados-iot/), o [firmware de ESP32 com FreeRTOS](/artigos/zig-esp32-freertos-firmware/), os fundamentos de [sockets TCP/UDP](/artigos/zig-networking-sockets-tcp-udp/) e a [interoperabilidade com C](/artigos/zig-interoperabilidade-c/). Para a parte de carreira, vale ligar com o guia de [carreira em embedded com Zig](/carreira/zig-para-embedded-carreira/) e a página de [vagas de Zig no Brasil](/carreira/vagas-zig-brasil/).

## Por que MQTT combina com Zig

O MQTT é um protocolo de mensageria leve sobre TCP criado para redes com banda limitada e dispositivos restritos. Em vez de o cliente falar diretamente com outro cliente, todo mundo publica e assina em um **broker** central usando *tópicos* hierárquicos — por exemplo `fabrica/linha1/sensor/temperatura`. Esse modelo pub/sub desacopla produtores e consumidores e lida bem com devices que desconectam o tempo todo.

As características que tornam o MQTT bom para embarcados são exatamente as que casam com a filosofia do Zig:

- **binário e compacto**: o cabeçalho fixo tem 2 bytes; cada mensagem carrega pouco overhead, o que importa quando você roda em um enlace de rádio de baixa taxa;
- **sem mágica de runtime**: não há coletor de lixo nem máquina virtual entre o seu código e o socket;
- **controle fino de memória**: você aloca buffers do tamanho exato para os pacotes, sem suprir as dependências de uma biblioteca genérica;
- **compila para qualquer alvo**: o mesmo cliente MQTT roda no seu notebook, em um Raspberry Pi e, com os [cuidados de cross-compilação](/artigos/zig-cross-compilation-guia/), direto no firmware.

Em suma, o MQTT resolve o problema de "como vários dispositivos conversam de forma resiliente", e o Zig resolve o problema de "como fazer isso sem desperdiçar RAM e sem perder previsibilidade".

## Como o MQTT funciona por baixo dos panos

Antes de escrever código, vale fixar os conceitos que aparecem em qualquer cliente:

- **Broker**: o servidor que recebe `PUBLISH` e replica para quem tem `SUBSCRIBE` casando com o tópico (Mosquitto, EMQX e HiveMQ são exemplos comuns).
- **Tópicos e wildcards**: tópicos são separados por `/`. O `+` casa um nível (`sensor/+/temp`) e o `#` casa vários níveis a partir dali (`fabrica/#`).
- **QoS (Quality of Service)**: `0` é fire-and-forget (no máximo uma vez), `1` exige `PUBACK` (pelo menos uma vez, pode duplicar) e `2` faz um handshake de quatro passos (exatamente uma vez, mais caro).
- **Mensagem retida**: o broker guarda a última mensagem publicada com a flag *retain* e a entrega para novos assinantes daquele tópico.
- **Last Will and Testament (LWT)**: uma mensagem que o broker publica em seu nome se a conexão cair sem um `DISCONNECT` limpo — útil para sinalizar que um device ficou offline.
- **Keepalive**: a cada N segundos sem tráfego, cliente e broker trocam `PINGREQ`/`PINGRESP`; se um lado parar de responder, a conexão é considerada morta.

As portas padrão são `1883` (TCP puro) e `8883` (MQTT sobre TLS). Brokers também costumam expor WebSocket em `8083`/`8443` para aplicações web.

## Duas abordagens: biblioteca C ou cliente nativo

Você tem dois caminhos, e a escolha depende do seu projeto:

1. **Usar a libmosquitto (ou Paho MQTT C) via FFI** — é a opção mais segura para produção. A libmosquitto é madura, trata reconexão, QoS 2 e TLS. Em Zig você a importa com `@cImport` e linka no `build.zig`, como mostrado no guia de [interoperabilidade com C](/artigos/zig-interoperabilidade-c/). O custo é a dependência externa e o acoplamento à API em C.
2. **Escrever um cliente MQTT nativo em Zig** — ideal para embarcados restritos, para quem quer eliminar dependências e para entender o protocolo. Funciona perfeitamente para QoS 0 e 1 (a esmagadora maioria dos casos de telemetria). É o que vamos detalhar a seguir.

A regra prática: use a libmosquitto quando precisar de QoS 2, sessões persistentes complexas ou TLS com mTLS corporativo; escreva o cliente em Zig quando quiser um binário mínimo, controle total e um footprint previsível em flash.

## O detalhe que define o protocolo: o remaining length

Todo pacote MQTT começa com um byte de tipo/flags seguido de um campo chamado **Remaining Length**, que diz quantos bytes vêm depois. Esse campo é um inteiro de comprimento variável (1 a 4 bytes, 7 bits úteis por byte, com o bit mais alto indicando continuação). É a parte mais fácil de errar — e onde o Zig brilha:

```zig
// Codifica o "remaining length" do MQTT (1 a 4 bytes).
fn encodeRemainingLength(out: *[4]u8, len: u32) usize {
    var written: usize = 0;
    var x: u32 = len;
    while (true) {
        var encoded_byte: u8 = @intCast(x % 128);
        x = x / 128;
        if (x > 0) encoded_byte |= 0x80; // continuation bit
        out[written] = encoded_byte;
        written += 1;
        if (x == 0) break;
    }
    return written;
}
```

Sabendo disso, o resto é só montar o payload na ordem certa. Essa função, sozinha, já elimina o atrito de 90% dos tutoriais de MQTT em outras linguagens.

## Conectando a um broker com um cliente nativo

Abaixo está o esqueleto de um cliente MQTT 3.1.1 em Zig que conecta a um broker local (por exemplo, um `mosquitto` rodando em `127.0.0.1:1883`), autentica, assina um tópico e fica lendo mensagens publicadas pelo broker. As APIs de `std.net` mudam entre versões do Zig; trate o trecho como a estrutura canônica e ajuste os nomes da sua versão (0.14+ / 0.15+).

```zig
const std = @import("std");
const net = std.net;

// Monta um pacote CONNECT do MQTT 3.1.1 (clean session, keepalive 60s).
fn buildConnect(buf: []u8, client_id: []const u8) []u8 {
    // Variable header: protocol name "MQTT", level 4, flags, keepalive.
    var vh: [10]u8 = .{
        0x00, 0x04, 'M', 'Q', 'T', 'T', // protocol name
        0x04,                            // protocol level (3.1.1)
        0x02,                            // clean session flag
        0x00, 60,                        // keepalive = 60s (big endian)
    };
    var pos: usize = 0;
    var rl: [4]u8 = undefined;
    const rl_len = encodeRemainingLength(&rl, vh.len + 2 + client_id.len);
    buf[pos] = 0x10; // CONNECT
    pos += 1;
    @memcpy(buf[pos .. pos + rl_len], &rl);
    pos += rl_len;
    @memcpy(buf[pos .. pos + vh.len], &vh);
    pos += vh.len;
    // Payload: client ID com prefixo de comprimento (u16 big endian).
    buf[pos] = 0x00;
    buf[pos + 1] = @intCast(client_id.len);
    pos += 2;
    @memcpy(buf[pos .. pos + client_id.len], client_id);
    pos += client_id.len;
    return buf[0..pos];
}

pub fn main() !void {
    const addr = try net.Address.parseIp("127.0.0.1", 1883);
    var stream = try net.tcpConnectToAddress(addr);
    defer stream.close();

    var buf: [256]u8 = undefined;
    const connect = buildConnect(&buf, "zig-client-01");
    try stream.writeAll(connect);

    // Lê o CONNACK (4 bytes: 0x20 0x02 0x00 <return code>).
    var connack: [4]u8 = undefined;
    _ = try stream.read(&connack);
    std.debug.assert(connack[3] == 0x00); // 0x00 = aceito

    // SUBSCRIBE no tópico "sensor/#" com QoS 0.
    const subscribe = [_]u8{
        0x82, 0x08,                  // SUBSCRIBE, remaining length 8
        0x00, 0x01,                  // packet identifier = 1
        0x00, 0x07, 's', 'e', 'n', 's', 'o', 'r', '/', '#',
        0x00, // requested QoS 0
    };
    try stream.writeAll(&subscribe);

    // Loop de leitura: imprime payloads de PUBLISH recebidos.
    var inbox: [512]u8 = undefined;
    while (true) {
        const n = try stream.read(&inbox);
        if (n == 0) break;
        // PUBLISH: byte 0 = 0x30 (QoS0). Ignora cabeçalho e tópico aqui.
        if (n > 0 and (inbox[0] & 0xF0) == 0x30) {
            const topic_len: usize =
                (@as(usize, inbox[2]) << 8) | inbox[3];
            const payload = inbox[4 + topic_len .. n];
            std.debug.print("recebido: {s}\n", .{payload});
        }
    }
}
```

Esse exemplo é deliberadamente minimal: ele não decodifica o `SUBACK`, não implementa reconexão nem keepalive. Mas ele é o esqueleto real — a partir dele, cada peça de robustez é um acréscimo local e testável, exatamente como nos padrões de [concorrência e rede em Zig](/tutoriais/zig-networking-sockets/).

## Publicando telemetria

Para publicar, monte um `PUBLISH`. O byte de tipo carrega flags: bit 3 é *DUP*, bits 1–2 são o QoS e o bit 0 é *retain*. Para QoS 0 sem retain, o byte é `0x30`:

```zig
fn buildPublish(buf: []u8, topic: []const u8, payload: []const u8) []u8 {
    var pos: usize = 0;
    var rl: [4]u8 = undefined;
    const remaining = 2 + topic.len + payload.len;
    const rl_len = encodeRemainingLength(&rl, @intCast(remaining));
    buf[pos] = 0x30; // PUBLISH, QoS0, no retain
    pos += 1;
    @memcpy(buf[pos .. pos + rl_len], &rl);
    pos += rl_len;
    buf[pos] = @intCast((topic.len >> 8) & 0xFF);
    buf[pos + 1] = @intCast(topic.len & 0xFF);
    pos += 2;
    @memcpy(buf[pos .. pos + topic.len], topic);
    pos += topic.len;
    @memcpy(buf[pos .. pos + payload.len], payload);
    pos += payload.len;
    return buf[0..pos];
}
```

O padrão que emerge é sempre o mesmo: calcular o *remaining length*, copiar o cabeçalho, copiar os comprimentos em big endian, copiar os dados. Depois do terceiro pacote, você escreve MQTT sem consultar a especificação.

## QoS, keepalive e reconexão

Para telemetria, QoS 0 basta: perde-se uma leitura ocasional e tudo bem. Para comandos (abrir uma válvula, acionar um atuador), suba para QoS 1, que exige um `PUBACK` do broker e reenvio em caso de timeout. Em Zig, o reenvio é trivial: mantenha o pacote em um buffer, dispare um timer com `std.time` e reenvie se o `PUBACK` não vier — o padrão de *timeout e retry* é o mesmo usado no guia de [circuit breaker, timeout e retry](/artigos/zig-circuit-breaker-timeout-retry/).

O **keepalive** evita que firewalls e NATs derrubem conexões ociosas: a cada `keepalive` segundos sem enviar nada, mande um `PINGREQ` (`0xC0 0x00`) e espere um `PINGRESP` (`0xD0 0x00`). Se o broker não responder dentro do tempo razoável, feche o socket e reconecte. Reconexão com *backoff* exponencial, limite de tentativas e estado salvo em flash é o que separa um protótipo de um device que sobrevive a quedas de energia e de rede — recomendações detalhadas estão no material sobre [sistemas de tempo real](/artigos/zig-real-time-systems/).

## Last Will e mensagens retidas

Configure o **Last Will** no `CONNECT` (na seção de variável header/payload) para que o broker publique, por exemplo, `dispositivos/zig-client-01/status = offline` se a sua conexão cair abruptamente. Combine com uma mensagem **retida** que você mesmo publica ao reconectar (`.../status = online`, com a flag *retain*): novos assinantes saberão instantaneamente o estado atual de cada device, sem precisar esperar a próxima publicação.

## TLS para MQTT

Rodar telemetria em texto puro na internet pública é um risco. Use a porta `8883` com TLS. Em embarcados com recursos, a libmosquitto cuida do handshake; em um cliente nativo, você encadeia o socket em `std.crypto.tls` (quando disponível no seu alvo) ou terceiriza para a biblioteca do sistema. Os cuidados com cadeia de certificados, validação de hostname e mTLS são os mesmos do guia de [TLS, HTTPS e mTLS em produção](/artigos/zig-tls-https-certificados-mtls/).

## Backpressure, fila local e observabilidade

Devices de campo produzem dados em rajadas. Se o broker estiver lento, não bloqueie a coleta: acumule em uma fila local em memória (ou em flash, se volátil) e drene quando o socket respirar. Esse padrão de fila de workers com descarte controlado é o do guia de [filas e workers em background](/artigos/zig-filas-workers-background/). E, como em qualquer sistema distribuído, exponha métricas: conexões estabelecidas, mensagens publicadas, *backoff* atual, reconexões — integrando com o que já existe em [observabilidade e Prometheus](/artigos/zig-observabilidade/).

## Quando NÃO usar MQTT

O MQTT não é sempre a resposta. Considere alternativas quando:

- o cliente é um navegador ou app que já fala HTTP/WebSocket — `SSE` e Websockets costumam bastar (veja [Server-Sent Events em Zig](/artigos/zig-server-sent-events-sse/));
- você precisa de mensageria entre serviços de backend comordering forte, partitions e replay — NATS, Kafka ou filas AMQP são mais adequados;
- o tráfego é consulta/resposta simples de baixa frequência — um HTTP REST enxuto é mais simples de operar.

O MQTT brilha no meio termo: muitos devices, mensagens pequenas e frequentes, tolerância a reconexão e baixo consumo de b banda.

## Checklist de produção

Antes de levar um cliente MQTT em Zig para campo, confirme que:

- o `client_id` é único por device (senão o broker derruba a conexão anterior);
- o keepalive está configurado e há `PINGREQ`/`PINGRESP` efetivos;
- há reconexão com backoff e limite de tentativas;
- o Last Will e a mensagem retida de status estão definidos;
- o TLS está habilitado em `8883` com validação de certificado (ou mTLS, se exigido);
- há fila/backpressure para rajadas e métricas exportadas;
- os buffers têm tamanho fixo conhecido, sem alocação no hot path;
- o QoS escolhido corresponde à criticidade de cada tópico (telemetria = 0, comando = 1).

## Perguntas frequentes

**Preciso de uma biblioteca para falar MQTT em Zig?** Não. O protocolo é simples o suficiente para um cliente nativo com algumas centenas de linhas, como mostrado aqui. Para QoS 2 e TLS corporativo, porém, a libmosquitto via FFI poupa tempo.

**Qual broker usar em desenvolvimento?** O Mosquitto é o padrão de fato. Rode `docker run -p 1883:1883 eclipse-mosquitto` e aponte seu cliente Zig para `127.0.0.1:1883`.

**MQTT 5 ou 3.1.1?** Muitos dispositivos embarcados ainda usam 3.1.1 (level 4), que é o deste guia. O MQTT 5 adiciona *shared subscriptions*, reason codes e properties, mas aumenta a complexidade do encoder.

**Qual o tamanho típico de um cliente MQTT em Zig?** Um cliente QoS 0/1 com reconexão e keepalive costuma ficar entre 4 e 8 KB de binário, dependendo do alvo — um dos motivos pelo qual o Zig é atrativo para firmware.

## Próximos passos

Para aprofundar, leia sobre [IoT e embarcados em Zig](/artigos/zig-embarcados-iot/), [firmware de ESP32 com FreeRTOS](/artigos/zig-esp32-freertos-firmware/), os fundamentos de [rede com sockets TCP/UDP](/artigos/zig-networking-sockets-tcp-udp/) e a [interoperabilidade com C](/artigos/zig-interoperabilidade-c/) para o caminho da libmosquitto. Para comparar com outra linguagem de sistemas forte em embarcados — onde o cenário MQTT costuma exigir bindar uma biblioteca C via crate `extern` — veja como o tema aparece em <a href="https://rustlang.com.br/artigos/rust-para-embarcados/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust para embarcados</a>: a diferença é que o Rust depende de bindings (`bindgen`/`cc`) para a pilha de rede embarcada, enquanto o Zig compila o mesmo código C junto no build e fala o protocolo diretamente, sem camada extra de FFI para o caso nativo.
