IoT Tutorials

Paho MQTT C Client Tutorial for Embedded Linux

A practical Paho MQTT C tutorial for embedded Linux: build the library with OpenSSL, write a TLS publisher and subscriber, cross-compile for ARM, and run it reliably under systemd.

Paho MQTT C client running on an embedded Linux board publishing sensor data over TLS to a cloud MQTT broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

The Paho MQTT C library is the standard choice when you need a small, dependency-light MQTT client on embedded Linux. In this tutorial you will build Paho MQTT C with OpenSSL, write a TLS publisher and a subscriber, cross-compile for an ARM board, and run the result as a resilient systemd service connected to a cloud broker.

Why Paho MQTT C for Embedded Linux

Eclipse Paho C is plain C99 with one optional dependency, OpenSSL, for TLS. It runs on anything from a Raspberry Pi to an industrial gateway with a few megabytes of RAM, supports MQTT 3.1, 3.1.1 and 5, and is packaged by most distributions and Yocto layers. If you are choosing between languages and libraries, the overview of MQTT client libraries puts it in context; if you prefer C++, Paho also offers a C++ wrapper on top of this library.

The library ships four variants. Pick the right one at link time:

Library API style TLS Typical use
paho-mqtt3c Synchronous MQTTClient_* No Simple tools on trusted networks
paho-mqtt3cs Synchronous MQTTClient_* Yes Most embedded publishers and subscribers
paho-mqtt3a Asynchronous MQTTAsync_* No Event-driven apps
paho-mqtt3as Asynchronous MQTTAsync_* Yes Event-driven apps with TLS

This tutorial uses paho-mqtt3cs: the synchronous API with TLS. "Synchronous" refers to connect and publish calls blocking until complete; incoming messages still arrive on a background thread through callbacks.

Installing or Building the Library

On Debian, Ubuntu or Raspberry Pi OS the packaged version is usually enough:

sudo apt update
sudo apt install -y build-essential libpaho-mqtt-dev libssl-dev ca-certificates

To get the latest release, or to cross-compile, build from source:

git clone https://github.com/eclipse-paho/paho.mqtt.c.git
cd paho.mqtt.c
cmake -B build -DPAHO_WITH_SSL=TRUE -DPAHO_BUILD_SAMPLES=FALSE -DPAHO_BUILD_STATIC=FALSE -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
sudo cmake --install build
sudo ldconfig

The official Paho C documentation documents every structure and function used below.

A Paho MQTT C Publisher Over TLS

This program connects to broker.connectmq.com on port 8883, verifies the server certificate against the system CA bundle, and publishes a reading every five seconds. It works anonymously against the public MQTT test broker, and reads credentials from environment variables when you move to a private broker.

publisher.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include "MQTTClient.h"

#define ADDRESS   "ssl://broker.connectmq.com:8883"
#define CA_BUNDLE "/etc/ssl/certs/ca-certificates.crt"
#define QOS       1
#define TIMEOUT   10000L

static volatile sig_atomic_t running = 1;
static void on_signal(int sig) { (void)sig; running = 0; }

static void connection_lost(void *ctx, char *cause) {
    (void)ctx;
    fprintf(stderr, "connection lost: %s\n", cause ? cause : "unknown");
}

static int message_arrived(void *ctx, char *topic, int len, MQTTClient_message *msg) {
    (void)ctx; (void)len;
    MQTTClient_freeMessage(&msg);
    MQTTClient_free(topic);
    return 1;
}

static int connect_broker(MQTTClient client) {
    MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
    MQTTClient_SSLOptions ssl = MQTTClient_SSLOptions_initializer;

    ssl.trustStore = CA_BUNDLE;
    ssl.enableServerCertAuth = 1;
    ssl.verify = 1;                       /* check the hostname too */
    ssl.sslVersion = MQTT_SSL_VERSION_TLS_1_2;

    opts.keepAliveInterval = 30;
    opts.cleansession = 1;
    opts.ssl = &ssl;
    opts.username = getenv("MQTT_USERNAME");   /* NULL = anonymous */
    opts.password = getenv("MQTT_PASSWORD");

    int rc = MQTTClient_connect(client, &opts);
    if (rc != MQTTCLIENT_SUCCESS)
        fprintf(stderr, "connect failed, rc=%d\n", rc);
    return rc;
}

int main(void) {
    const char *client_id = getenv("MQTT_CLIENT_ID");
    const char *prefix = getenv("MQTT_TOPIC_PREFIX");
    char id_buf[64], topic[256], payload[128];

    if (!client_id) {
        snprintf(id_buf, sizeof id_buf, "paho-c-%d", (int)getpid());
        client_id = id_buf;
    }
    if (!prefix) prefix = "connectmq/demo/paho-c-4821";
    snprintf(topic, sizeof topic, "%s/gateway1/temperature", prefix);

    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    MQTTClient client;
    int rc = MQTTClient_create(&client, ADDRESS, client_id,
                               MQTTCLIENT_PERSISTENCE_NONE, NULL);
    if (rc != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "create failed, rc=%d\n", rc);
        return EXIT_FAILURE;
    }
    MQTTClient_setCallbacks(client, NULL, connection_lost, message_arrived, NULL);

    int backoff = 1;
    while (running && connect_broker(client) != MQTTCLIENT_SUCCESS) {
        sleep(backoff);
        if (backoff < 60) backoff *= 2;
    }
    printf("connected as %s, publishing to %s\n", client_id, topic);

    srand((unsigned)time(NULL));
    while (running) {
        if (!MQTTClient_isConnected(client)) {
            if (connect_broker(client) != MQTTCLIENT_SUCCESS) { sleep(5); continue; }
        }

        double temp = 20.0 + (rand() % 100) / 10.0;
        int n = snprintf(payload, sizeof payload,
                         "{\"ts\":%ld,\"temperature\":%.1f}", (long)time(NULL), temp);

        MQTTClient_message msg = MQTTClient_message_initializer;
        MQTTClient_deliveryToken token;
        msg.payload = payload;
        msg.payloadlen = n;
        msg.qos = QOS;
        msg.retained = 0;

        rc = MQTTClient_publishMessage(client, topic, &msg, &token);
        if (rc == MQTTCLIENT_SUCCESS)
            rc = MQTTClient_waitForCompletion(client, token, TIMEOUT);
        if (rc != MQTTCLIENT_SUCCESS)
            fprintf(stderr, "publish failed, rc=%d\n", rc);
        else
            printf("sent %s\n", payload);

        sleep(5);
    }

    MQTTClient_disconnect(client, TIMEOUT);
    MQTTClient_destroy(&client);
    return EXIT_SUCCESS;
}

Build and run:

gcc -Wall -O2 -o publisher publisher.c -lpaho-mqtt3cs
./publisher

A few lines deserve explanation. ssl.verify = 1 turns on hostname verification, which is off by default; without it, any valid certificate for any host would be accepted. Setting MQTTClient_setCallbacks before connecting puts the client in multithreaded mode, so the library runs keep-alive and receive processing on its own thread and connection_lost tells you when the link drops. The publish loop checks MQTTClient_isConnected and reconnects with its own retry, because the synchronous API does not reconnect automatically.

A Subscriber With Callbacks

The subscriber reuses the same connection logic and does its work in message_arrived. Replace main with this (keeping the helpers above):

static int on_message(void *ctx, char *topic, int len, MQTTClient_message *msg) {
    (void)ctx; (void)len;
    printf("%s: %.*s\n", topic, msg->payloadlen, (char *)msg->payload);
    MQTTClient_freeMessage(&msg);
    MQTTClient_free(topic);
    return 1;   /* 1 = message handled; 0 asks the library to redeliver */
}

int main(void) {
    const char *prefix = getenv("MQTT_TOPIC_PREFIX");
    char filter[256];
    if (!prefix) prefix = "connectmq/demo/paho-c-4821";
    snprintf(filter, sizeof filter, "%s/+/temperature", prefix);

    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    MQTTClient client;
    MQTTClient_create(&client, ADDRESS, "paho-c-sub-4821",
                      MQTTCLIENT_PERSISTENCE_NONE, NULL);
    MQTTClient_setCallbacks(client, NULL, connection_lost, on_message, NULL);

    while (running) {
        if (!MQTTClient_isConnected(client)) {
            if (connect_broker(client) == MQTTCLIENT_SUCCESS) {
                MQTTClient_subscribe(client, filter, QOS);
                printf("subscribed to %s\n", filter);
            } else {
                sleep(5);
                continue;
            }
        }
        sleep(1);
    }
    MQTTClient_disconnect(client, TIMEOUT);
    MQTTClient_destroy(&client);
    return 0;
}

Freeing both the message and the topic string is mandatory; forgetting either is the most common memory leak in Paho C programs. Keep the callback short. If processing is slow (writing to flash, calling an HTTP API), copy the payload to a queue and return, because the callback runs on the library's receive thread.

You can also watch the publisher with no code at all:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-4821 -t "connectmq/demo/paho-c-4821/#" -v

Cross-Compiling for an ARM Board

For Yocto or Buildroot images, Paho C has ready recipes. For a quick manual cross build, point CMake at your toolchain and a cross-compiled OpenSSL:

cmake -B build-arm \
  -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
  -DCMAKE_SYSTEM_NAME=Linux \
  -DPAHO_WITH_SSL=TRUE \
  -DOPENSSL_ROOT_DIR=/opt/sysroot/usr \
  -DPAHO_BUILD_SHARED=TRUE -DPAHO_BUILD_SAMPLES=FALSE \
  -DCMAKE_INSTALL_PREFIX=/opt/paho-arm -DCMAKE_INSTALL_LIBDIR=lib
cmake --build build-arm -j"$(nproc)"
cmake --install build-arm

aarch64-linux-gnu-gcc -O2 -o publisher publisher.c \
  -I/opt/paho-arm/include -L/opt/paho-arm/lib -lpaho-mqtt3cs -lssl -lcrypto -lpthread

# Copy the binary and the shared library to the board
scp publisher /opt/paho-arm/lib/libpaho-mqtt3cs.so* root@gateway:/usr/local/lib/

On the board, move publisher to /usr/local/bin and run ldconfig. Paho can also be built as a static library (-DPAHO_BUILD_STATIC=TRUE) to avoid shipping a separate .so, but then you own OpenSSL updates for that binary. Check that the target image contains a CA bundle; minimal images often do not, and TLS fails with a certificate verification error even though the code is correct. If the same gateway also drives a local touchscreen, the Qt MQTT client tutorial shows how to build the HMI side in C++, and testing MQTT clients covers how to put this code under CI.

QoS, Persistence and Offline Buffering

Gateways lose connectivity, so decide up front what happens to data during an outage. The client above uses MQTTCLIENT_PERSISTENCE_NONE, which keeps in-flight QoS 1 messages in memory only; if the process restarts mid-delivery, those messages are gone. Paho can instead persist in-flight messages to disk: pass MQTTCLIENT_PERSISTENCE_DEFAULT and a directory path as the last two arguments of MQTTClient_create, connect with cleansession = 0, and the library resends anything not yet acknowledged after a reconnect. On flash storage, weigh that against write wear, because persisting every message on a cheap SD card shortens its life.

Persistence only covers messages already handed to the library. Readings taken while the link is down never reach MQTTClient_publishMessage in the example, because the loop skips publishing until it reconnects. If you need every reading, write them to a local ring buffer (a file or an SQLite database) and drain it after reconnecting, oldest first, with the original timestamp in the payload so the back end can place each value correctly.

QoS choice follows the data. Periodic telemetry that is replaced every few seconds is fine at QoS 0 or 1; commands and alarms deserve QoS 1. QoS 2 adds an extra round trip and is rarely needed on gateways. The free ConnectMQ Serverless plan supports QoS up to 1, and paid plans add QoS 2. The MQTT QoS levels guide explains the delivery guarantees in detail.

Running as a systemd Service

/etc/systemd/system/mqtt-publisher.service:

[Unit]
Description=Paho MQTT C publisher
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/publisher
Environment=MQTT_CLIENT_ID=YOUR_CLIENT_ID
Environment=MQTT_USERNAME=YOUR_USERNAME
Environment=MQTT_PASSWORD=YOUR_PASSWORD
Environment=MQTT_TOPIC_PREFIX=acme/plant1/gateway1
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-publisher
journalctl -u mqtt-publisher -f

The credentials above come from your private broker. With a managed MQTT broker like ConnectMQ, you create the broker in the portal and receive a generated username, password and client ID; client IDs are bound to those credentials, so give each gateway its own set. For anything beyond a handful of devices, move secrets into an EnvironmentFile readable only by root.

Return Codes and Troubleshooting

Return code / symptom Meaning What to check
-1 (MQTTCLIENT_FAILURE) on connect with TLS Handshake or certificate failure CA bundle path, system clock (TLS fails if the date is wrong), hostname
4 Bad username or password Credentials, trailing newline in env file
5 Not authorised Client ID does not match issued credentials
2 Identifier rejected Empty or duplicate client ID
Connect hangs on 8883 Firewall blocks the port Test with openssl s_client -connect broker.connectmq.com:8883
Memory grows over days Messages or topics not freed MQTTClient_freeMessage and MQTTClient_free in every callback path

The clock issue is worth stressing for embedded Linux: boards without a real-time clock boot in 1970, and every certificate looks "not yet valid" until NTP syncs. Order your service after time-sync.target if that bites you. More TLS failure patterns are covered in MQTT TLS handshake failed.

Get Started with ConnectMQ

Your C gateway is ready for a real broker. Create a free Serverless broker at app.connectmq.com, no credit card needed, drop the generated credentials into the systemd unit, and watch messages arrive in the portal's live topic explorer. When the fleet grows, the same binary connects to paid, dedicated or BYOC brokers without code changes.

Frequently Asked Questions

Link paho-mqtt3cs for the synchronous API with TLS, or paho-mqtt3as for the asynchronous API with TLS. The variants without the trailing s have no SSL support.

Does the Paho MQTT C client reconnect automatically?

The synchronous MQTTClient API does not, so check MQTTClient_isConnected and reconnect yourself. The asynchronous MQTTAsync API offers an automaticReconnect option.

Does Paho C support MQTT 5?

Yes. Create the client with MQTTClient_createWithOptions and MQTTVERSION_5, then use the MQTTClient_connect5 and MQTTClient_publish5 family of functions; ConnectMQ accepts both MQTT 3.1.1 and 5.0.

How much memory does Paho MQTT C need?

It is small enough for typical embedded Linux gateways, but the exact footprint depends on your build options and OpenSSL. Measure on your target with the variant you actually link.

Related articles

Best MQTT CLI Tools for Testing, Scripting and Debugging

A hands-on comparison of mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, the MQTT.js CLI, curl and mqttui, with the same test run against broker.connectmq.com so you can choose the right tool for scripting and debugging.

8 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.