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:
# Debian / Ubuntu
sudo apt install libpaho-mqtt-dev
# macOS (Homebrew)
brew install libpaho-mqttFor the latest release, or to control build options, compile it with CMake:
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 onlyYou 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:
| Library | API | TLS |
|---|---|---|
paho-mqtt3c | MQTTClient (sync) | No |
paho-mqtt3cs | MQTTClient (sync) | Yes |
paho-mqtt3a | MQTTAsync | No |
paho-mqtt3as | MQTTAsync | Yes |
A synchronous publisher
The synchronous API reads top to bottom: create, connect, publish, wait for delivery, disconnect.
#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;
}gcc pub.c -o pub -lpaho-mqtt3c
./pubBefore 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.
#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;
}gcc sub_async.c -o sub_async -lpaho-mqtt3a
./sub_asyncRun ./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_freeMessageandMQTTAsync_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:
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:
#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;gcc pub_tls.c -o pub_tls -lpaho-mqtt3csWithout 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:
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
| Code | Meaning | Typical fix |
|---|---|---|
-1 (MQTTCLIENT_FAILURE) | Generic failure, often host unreachable or TLS handshake failed | Check host, port, firewall, certificates |
-10 (MQTTCLIENT_SSL_NOT_SUPPORTED) | ssl:// URI with a non-TLS library | Link paho-mqtt3cs / paho-mqtt3as |
-14 (MQTTCLIENT_BAD_PROTOCOL) | Unknown URI scheme | Use tcp://, ssl://, ws:// or wss:// |
4 | CONNACK: bad user name or password | Check credentials |
5 | CONNACK: not authorized | Check 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
- Run
sub_async, thenpub, and confirm the round trip; repeat with the online client on one side. - 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.
- Set
MQTT_PASSto a wrong value and confirm the failure callback reports it. - 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.


