Tutorials

MQTT in C with Eclipse Paho C: Sync and Async Client Examples

Use MQTT in C with the Eclipse Paho C library: install or build it, a sync publisher, an async subscriber with auto reconnect, TLS, MQTT 5 and return codes.

Updated · 10 min read

MQTT in C with Eclipse Paho C: Sync and Async Client Examples

Eclipse Paho C is the reference MQTT client library for C on Linux, macOS and Windows. It offers two APIs: MQTTClient, a synchronous API where each call blocks until it is done, and MQTTAsync, a non-blocking API driven by callbacks with built-in automatic reconnect. Both support MQTT 3.1.1 and MQTT 5, TLS via OpenSSL and WebSockets. This tutorial installs the library, builds a synchronous publisher and an asynchronous subscriber, then adds TLS and MQTT 5.

For microcontrollers without an operating system, Paho C is usually too heavy; look at coreMQTT on FreeRTOS instead.

Install or build the library

Packages are the quickest route:

Install from packages
# Debian / Ubuntu
sudo apt install libpaho-mqtt-dev

# macOS (Homebrew)
brew install libpaho-mqtt

For the latest release, or to control build options, compile it with CMake:

Build from source with TLS
git clone https://github.com/eclipse/paho.mqtt.c.git
cd paho.mqtt.c
cmake -Bbuild -H. -DPAHO_WITH_SSL=ON -DPAHO_ENABLE_TESTING=OFF
sudo cmake --build build/ --target install
sudo ldconfig   # Linux only

You need a C compiler, CMake and the OpenSSL development package (libssl-dev) for PAHO_WITH_SSL=ON. The build produces four libraries; pick the one that matches your program:

LibraryAPITLS
paho-mqtt3cMQTTClient (sync)No
paho-mqtt3csMQTTClient (sync)Yes
paho-mqtt3aMQTTAsyncNo
paho-mqtt3asMQTTAsyncYes

A synchronous publisher

The synchronous API reads top to bottom: create, connect, publish, wait for delivery, disconnect.

pub.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "MQTTClient.h"

#define ADDRESS  "tcp://broker.connectmq.com:1883"
#define CLIENTID "testmqtt-c-sync-pub"
#define TOPIC    "testmqtt/c-paho/hello"
#define TIMEOUT  10000L

int main(void)
{
    MQTTClient client;
    MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
    MQTTClient_message msg = MQTTClient_message_initializer;
    MQTTClient_deliveryToken token;
    int rc;

    rc = MQTTClient_create(&client, ADDRESS, CLIENTID,
                           MQTTCLIENT_PERSISTENCE_NONE, NULL);
    if (rc != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "create failed: %d\n", rc);
        return EXIT_FAILURE;
    }

    conn_opts.keepAliveInterval = 30;
    conn_opts.cleansession = 1;
    conn_opts.username = getenv("MQTT_USER");   /* NULL is fine for open brokers */
    conn_opts.password = getenv("MQTT_PASS");

    if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "connect failed: %d (%s)\n", rc, MQTTClient_strerror(rc));
        MQTTClient_destroy(&client);
        return EXIT_FAILURE;
    }

    const char *payload = "hello from Paho C";
    msg.payload = (void *)payload;
    msg.payloadlen = (int)strlen(payload);
    msg.qos = 1;
    msg.retained = 0;

    MQTTClient_publishMessage(client, TOPIC, &msg, &token);
    rc = MQTTClient_waitForCompletion(client, token, TIMEOUT);
    printf("delivery %s (token %d)\n", rc == MQTTCLIENT_SUCCESS ? "confirmed" : "failed", token);

    MQTTClient_disconnect(client, 1000);
    MQTTClient_destroy(&client);
    return rc == MQTTCLIENT_SUCCESS ? EXIT_SUCCESS : EXIT_FAILURE;
}
Compile and run
gcc pub.c -o pub -lpaho-mqtt3c
./pub

Before running it, open the online MQTT client and subscribe to testmqtt/c-paho/# — the message shows up there as soon as pub prints "delivery confirmed". With QoS 1, MQTTClient_waitForCompletion returns only after the broker's PUBACK, so success really means the broker has the message.

An asynchronous subscriber with auto reconnect

For a long-running process, MQTTAsync is the better fit. Nothing blocks; you register callbacks and the library's background thread calls them. The important part is the connected callback: it runs after every successful connection, including automatic reconnects, so it is the right place to subscribe.

sub_async.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "MQTTAsync.h"

#define ADDRESS  "tcp://broker.connectmq.com:1883"
#define CLIENTID "testmqtt-c-async-sub"
#define TOPIC    "testmqtt/c-paho/#"

static void on_subscribe(void *ctx, MQTTAsync_successData *resp)
{
    printf("subscribed to %s\n", TOPIC);
}

static void on_subscribe_failure(void *ctx, MQTTAsync_failureData *resp)
{
    printf("subscribe failed: %d\n", resp ? resp->code : 0);
}

/* Runs after the first connect and after every automatic reconnect. */
static void on_connected(void *ctx, char *cause)
{
    MQTTAsync client = (MQTTAsync)ctx;
    MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer;
    opts.onSuccess = on_subscribe;
    opts.onFailure = on_subscribe_failure;
    opts.context = client;
    printf("connected (%s)\n", cause ? cause : "");
    MQTTAsync_subscribe(client, TOPIC, 1, &opts);
}

static void on_connect_failure(void *ctx, MQTTAsync_failureData *resp)
{
    printf("connect failed: %d %s\n", resp ? resp->code : 0,
           resp && resp->message ? resp->message : "");
}

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

static int on_message(void *ctx, char *topic, int topic_len, MQTTAsync_message *m)
{
    printf("received %s: %.*s\n", topic, m->payloadlen, (char *)m->payload);
    MQTTAsync_freeMessage(&m);
    MQTTAsync_free(topic);
    return 1;   /* 1 = message handled */
}

int main(void)
{
    MQTTAsync client;
    MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer;
    MQTTAsync_disconnectOptions disc_opts = MQTTAsync_disconnectOptions_initializer;
    int rc;

    if ((rc = MQTTAsync_create(&client, ADDRESS, CLIENTID,
                               MQTTCLIENT_PERSISTENCE_NONE, NULL)) != MQTTASYNC_SUCCESS) {
        fprintf(stderr, "create failed: %d\n", rc);
        return EXIT_FAILURE;
    }
    MQTTAsync_setCallbacks(client, client, on_connection_lost, on_message, NULL);
    MQTTAsync_setConnected(client, client, on_connected);

    conn_opts.keepAliveInterval = 30;
    conn_opts.cleansession = 1;
    conn_opts.automaticReconnect = 1;
    conn_opts.minRetryInterval = 1;    /* seconds, doubles up to max */
    conn_opts.maxRetryInterval = 30;
    conn_opts.onFailure = on_connect_failure;
    conn_opts.context = client;
    conn_opts.username = getenv("MQTT_USER");
    conn_opts.password = getenv("MQTT_PASS");

    if ((rc = MQTTAsync_connect(client, &conn_opts)) != MQTTASYNC_SUCCESS) {
        fprintf(stderr, "connect start failed: %d\n", rc);
        return EXIT_FAILURE;
    }

    printf("press Enter to quit\n");
    getchar();

    MQTTAsync_disconnect(client, &disc_opts);
    sleep(1);
    MQTTAsync_destroy(&client);
    return EXIT_SUCCESS;
}
Compile and run
gcc sub_async.c -o sub_async -lpaho-mqtt3a
./sub_async

Run ./pub in a second terminal, or publish from the browser client, and the subscriber prints every message. Three rules keep MQTTAsync code healthy:

  • Free what the library gives you. The message callback must call MQTTAsync_freeMessage and MQTTAsync_free(topic), or the process leaks memory with every message.
  • Return quickly from callbacks. They run on the library's thread; long work there delays keep-alive handling. Hand messages to your own queue or worker.
  • Subscribe in the connected callback. With a clean session the broker forgets subscriptions on disconnect; resubscribing there restores them after each automatic reconnect.

Building with CMake

The installed library ships a CMake package, so larger projects do not need hand-written linker flags:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(mqtt_c_demo C)

find_package(eclipse-paho-mqtt-c REQUIRED)

add_executable(pub pub.c)
target_link_libraries(pub eclipse-paho-mqtt-c::paho-mqtt3c)

add_executable(sub_async sub_async.c)
target_link_libraries(sub_async eclipse-paho-mqtt-c::paho-mqtt3a)

Use the paho-mqtt3cs / paho-mqtt3as targets once you add TLS.

TLS and credentials

Change the URI scheme to ssl:// and the port to 8883, fill in SSL options and link the TLS variant of the library:

TLS options (sync API)
#define ADDRESS "ssl://broker.connectmq.com:8883"

MQTTClient_SSLOptions ssl_opts = MQTTClient_SSLOptions_initializer;
ssl_opts.enableServerCertAuth = 1;
ssl_opts.verify = 1;                 /* check the host name in the certificate */
/* ssl_opts.trustStore = "ca.pem";      private CA only */
/* ssl_opts.keyStore = "client.pem";    client certificate (mutual TLS) */
conn_opts.ssl = &ssl_opts;
Link the TLS library
gcc pub_tls.c -o pub_tls -lpaho-mqtt3cs

Without a trustStore, OpenSSL's default certificate locations are used, which works for brokers with publicly trusted certificates. The async API has the same fields in MQTTAsync_SSLOptions. When the handshake fails, rule out the certificate chain first with the steps in testing an MQTT TLS connection.

MQTT 5 with Paho C

MQTT 5 needs a create option, a v5 initializer for the connect options and the ...5 variants of the calls, which return reason codes:

MQTT 5 connect (sync API)
MQTTClient_createOptions create_opts = MQTTClient_createOptions_initializer;
create_opts.MQTTVersion = MQTTVERSION_5;
MQTTClient_createWithOptions(&client, ADDRESS, CLIENTID,
                             MQTTCLIENT_PERSISTENCE_NONE, NULL, &create_opts);

MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer5;
conn_opts.cleanstart = 1;
MQTTProperties props = MQTTProperties_initializer;

MQTTResponse resp = MQTTClient_connect5(client, &conn_opts, &props, NULL);
if (resp.reasonCode != MQTTREASONCODE_SUCCESS)
    fprintf(stderr, "connect: %s\n", MQTTReasonCode_toString(resp.reasonCode));
MQTTResponse_free(resp);

Publishing and subscribing work the same way through MQTTClient_publishMessage5 and MQTTClient_subscribe5. Look up any reason code in the MQTT reason code reference.

Return codes you will see

CodeMeaningTypical fix
-1 (MQTTCLIENT_FAILURE)Generic failure, often host unreachable or TLS handshake failedCheck host, port, firewall, certificates
-10 (MQTTCLIENT_SSL_NOT_SUPPORTED)ssl:// URI with a non-TLS libraryLink paho-mqtt3cs / paho-mqtt3as
-14 (MQTTCLIENT_BAD_PROTOCOL)Unknown URI schemeUse tcp://, ssl://, ws:// or wss://
4CONNACK: bad user name or passwordCheck credentials
5CONNACK: not authorizedCheck the broker's ACLs for this client

Positive codes 1–5 are the broker's MQTT 3.1.1 CONNACK return codes, explained in detail in CONNACK reason codes. Print them with MQTTClient_strerror so logs are readable.

Testing your client

  1. Run sub_async, then pub, and confirm the round trip; repeat with the online client on one side.
  2. Disconnect the network for longer than the keep-alive and reconnect it: you should see "connection lost", then "connected" and "subscribed" again — the automatic reconnect at work. Reconnect best practices covers backoff for whole fleets.
  3. Set MQTT_PASS to a wrong value and confirm the failure callback reports it.
  4. Run under Valgrind or AddressSanitizer for a few thousand messages to prove the message callback frees everything.

Subscribe to the demo topic in your browser

Paho C is small, portable and has been used in production for years. Start with MQTTClient for tools and tests, move to MQTTAsync with automatic reconnect for services, link the s libraries when you add TLS, and free every message you receive. If you need C++ or Rust on top, Paho C++ in MQTT in C++ and paho-mqtt for Rust both wrap this library.

Frequently asked questions

What is the difference between MQTTClient and MQTTAsync in Paho C?

MQTTClient is the synchronous API: calls such as MQTTClient_connect block until they complete, which keeps simple programs short. MQTTAsync never blocks; every operation takes success and failure callbacks, and it adds features such as automatic reconnect and offline buffering. Use MQTTAsync for long-running services.

Which Paho C library should I link?

paho-mqtt3c for the synchronous API without TLS, paho-mqtt3cs for synchronous with TLS, paho-mqtt3a for asynchronous without TLS and paho-mqtt3as for asynchronous with TLS. Linking a non-TLS library and connecting to an ssl:// URI fails with return code -10 (SSL not supported).

What does return code 5 from MQTTClient_connect mean?

Positive codes 1 to 5 are the MQTT 3.1.1 CONNACK return codes from the broker. 5 means not authorized and 4 means bad user name or password. Negative codes come from the library itself, for example -1 for a general failure such as an unreachable host.