Tutorials

MQTT in PHP with php-mqtt/client

Use MQTT from PHP with php-mqtt/client: install it, publish with QoS 1, run a subscriber loop, add TLS and Last Will, and use the Laravel package.

Updated · 8 min read

MQTT in PHP with php-mqtt/client

The standard way to use MQTT in PHP is the php-mqtt/client library: install it with Composer, create an MqttClient, connect(), then publish() from anywhere and subscribe() with a closure inside a long-running CLI loop. It speaks MQTT 3.1 and 3.1.1 over TCP or TLS, needs no PHP extension beyond the defaults, and has an official Laravel wrapper.

This tutorial covers publishing (including the QoS 1 detail most people miss), a robust subscriber, TLS with credentials, Last Will, and how to test each piece.

Install

php-mqtt/client requires PHP 8 and Composer:

Install the library
composer require php-mqtt/client

There are two very different ways PHP talks to a broker, and mixing them up causes most problems:

RoleWhere it runsLifetimeExample
PublisherWeb request (PHP-FPM, Apache), cron, queue jobConnect, publish, disconnect in millisecondsSend a command when a user clicks a button
SubscriberCLI process (php subscriber.php)Hours or days, supervisedStore sensor readings in a database

PHP-FPM workers are recycled and requests time out, so never try to keep a subscription open inside a web request.

Publish a message

publish.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;

$clientId = 'testmqtt-php-' . bin2hex(random_bytes(4));
$mqtt = new MqttClient('broker.connectmq.com', 1883, $clientId, MqttClient::MQTT_3_1_1);

$settings = (new ConnectionSettings)
    ->setConnectTimeout(5)
    ->setKeepAliveInterval(30);

$mqtt->connect($settings, true);          // true = clean session

$payload = json_encode(['temp' => 21.5, 'ts' => time()]);
$mqtt->publish('testmqtt/php/sensors/room1', $payload, MqttClient::QOS_AT_LEAST_ONCE);

$mqtt->loop(true, true);                  // wait for the PUBACK, then return
$mqtt->disconnect();
echo "published\n";

Two details matter:

  • The random client ID. Every request is a separate connection. If two concurrent requests used the same fixed ID, the broker would disconnect the first one — see duplicate client ID. The client ID generator shows good formats.
  • loop(true, true) after a QoS 1 or 2 publish. publish() only queues the message; the acknowledgement is handled inside the loop. The second argument tells loop() to return as soon as nothing is pending. With QoS 0 you can disconnect straight away. MQTT QoS levels explains the trade-off.

Opening a connection per request is fine for occasional events. If a page publishes on every hit, or you send bursts of messages, push the work to a queue worker that keeps one connection open — the pattern shown in MQTT in Laravel.

Retained messages for state

The fourth argument of publish() is the retain flag. A retained message is stored by the broker and delivered immediately to every new subscriber, which makes it ideal for "current state" topics such as a device configuration that a PHP admin panel edits:

Publish and clear retained state
$config = ['interval' => 60, 'unit' => 'C'];
$mqtt->publish('testmqtt/php/devices/42/config', json_encode($config), MqttClient::QOS_AT_LEAST_ONCE, true);

// An empty retained payload deletes the stored message
$mqtt->publish('testmqtt/php/devices/42/config', '', MqttClient::QOS_AT_LEAST_ONCE, true);
$mqtt->loop(true, true);

Use retain for state, not for events: a retained "button pressed" message would be replayed to every client that subscribes later. Keep payloads as compact JSON with a timestamp, so consumers can tell how old the state is.

Subscribe in a CLI worker

A subscriber registers a closure per topic filter and then calls loop(true), which blocks and dispatches messages until you interrupt it:

subscriber.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;

$mqtt = new MqttClient('broker.connectmq.com', 1883, 'testmqtt-php-worker', MqttClient::MQTT_3_1_1);
$mqtt->connect((new ConnectionSettings)->setKeepAliveInterval(30), true);

$mqtt->subscribe('testmqtt/php/sensors/+', function (string $topic, string $message, bool $retained) {
    $data = json_decode($message, true);
    if (!is_array($data)) {
        fwrite(STDERR, "invalid JSON on $topic\n");
        return;
    }
    printf("%s%s temp=%s\n", $topic, $retained ? ' (retained)' : '', $data['temp'] ?? '?');
}, MqttClient::QOS_AT_LEAST_ONCE);

// Stop cleanly on Ctrl+C or a SIGTERM from systemd / Docker
pcntl_async_signals(true);
pcntl_signal(SIGINT, fn () => $mqtt->interrupt());
pcntl_signal(SIGTERM, fn () => $mqtt->interrupt());

$mqtt->loop(true);
$mqtt->disconnect();

The + wildcard matches one topic level, so this filter receives testmqtt/php/sensors/room1 but not testmqtt/php/sensors/room1/battery. Check filters with the topic matcher before deploying.

Run php subscriber.php in one terminal and php publish.php in another. You should see the reading printed within milliseconds.

Survive disconnects

A worker that runs for weeks will eventually lose its connection: broker restarts, NAT timeouts, deployments. The library throws an exception from loop() when that happens. The simplest robust pattern is an outer loop that reconnects with back-off and subscribes again:

worker.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\MqttClientException;
use PhpMqtt\Client\MqttClient;

$settings = (new ConnectionSettings)
    ->setKeepAliveInterval(30)
    ->setLastWillTopic('testmqtt/php/worker/status')
    ->setLastWillMessage('offline')
    ->setLastWillQualityOfService(MqttClient::QOS_AT_LEAST_ONCE)
    ->setRetainLastWill(true);

$delay = 1;
while (true) {
    try {
        $mqtt = new MqttClient('broker.connectmq.com', 1883, 'testmqtt-php-worker', MqttClient::MQTT_3_1_1);
        $mqtt->connect($settings, true);
        $mqtt->publish('testmqtt/php/worker/status', 'online', MqttClient::QOS_AT_LEAST_ONCE, true);
        $mqtt->subscribe('testmqtt/php/sensors/+', function (string $topic, string $message) {
            echo "$topic $message\n";
        }, MqttClient::QOS_AT_LEAST_ONCE);

        $delay = 1;                       // connected: reset back-off
        $mqtt->loop(true);
    } catch (MqttClientException $e) {
        fwrite(STDERR, 'MQTT error: ' . $e->getMessage() . "\n");
        sleep($delay);
        $delay = min($delay * 2, 30);
    }
}

The Last Will publishes offline (retained) if the worker dies without a clean disconnect, and the worker overwrites it with online after each connect — a cheap health indicator other services can subscribe to. MQTT Last Will covers the details.

Using a clean session here is deliberate: the worker resubscribes on every connect, so it does not depend on the broker storing anything. If you need messages queued while the worker is offline, switch to connect($settings, false) with a fixed client ID and test it against your own private broker — the shared broker.connectmq.com endpoint keeps no offline sessions. See persistent sessions.

php-mqtt/client also has setReconnectAutomatically(true) with setMaxReconnectAttempts() and setDelayBetweenReconnectAttempts() (milliseconds), which is handy for publishers that keep a connection open between jobs.

TLS and authentication

Production brokers require TLS on port 8883 and credentials. Only the port and the settings change:

tls.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;

$settings = (new ConnectionSettings)
    ->setUsername(getenv('MQTT_USER'))
    ->setPassword(getenv('MQTT_PASS'))
    ->setUseTls(true)
    ->setTlsVerifyPeer(true)
    ->setTlsVerifyPeerName(true)
    ->setTlsSelfSignedAllowed(false);
    // ->setTlsCertificateAuthorityFile('/path/to/ca.pem') for a private CA

$mqtt = new MqttClient('broker.connectmq.com', 8883, 'testmqtt-php-' . bin2hex(random_bytes(4)), MqttClient::MQTT_3_1_1);
$mqtt->connect($settings, true);
$mqtt->publish('testmqtt/php/secure', 'hello over TLS', MqttClient::QOS_AT_LEAST_ONCE);
$mqtt->loop(true, true);
$mqtt->disconnect();

Without an explicit CA file, PHP's OpenSSL uses the system trust store (or openssl.cafile in php.ini). Keep peer verification on; disabling it hides real certificate problems.

Using it in Laravel

The same authors maintain php-mqtt/laravel-client, which reads connection settings from config/mqtt-client.php and .env and exposes a facade:

Install the Laravel package
composer require php-mqtt/laravel-client
php artisan vendor:publish --provider="PhpMqtt\Client\MqttClientServiceProvider" --tag="config"
Anywhere in your app
use PhpMqtt\Client\Facades\MQTT;

MQTT::publish('testmqtt/php/laravel/hello', 'Hello from Laravel');

For subscribing you call MQTT::connection() inside an Artisan command and run loop(true) there. The full pattern — queued jobs for publishing, a supervised command for subscribing, and tests — is in MQTT in Laravel.

Common errors

SymptomCauseFix
ConnectingToBrokerFailedException with "Establishing a connection to the MQTT broker failed"Wrong host or port, firewall, TLS on a plain port (or the reverse)Match port and setUseTls(): 1883 plain, 8883 TLS
Connection refused after CONNECTWrong credentials or not authorizedCheck username, password and ACLs
Worker disconnects every few secondsTwo processes share one client IDUnique IDs per process, random IDs per request
QoS 1 message missingScript disconnected before the PUBACKloop(true, true) before disconnect()
Call to undefined function pcntl_async_signals()pcntl unavailable (Windows or web SAPI)Only use signal handling in CLI on Unix

Test it

Watch what your PHP code publishes with a terminal subscriber or the online MQTT client:

Watch the PHP topics
mosquitto_sub -h broker.connectmq.com -p 1883 -t 'testmqtt/php/#' -v

Then feed your subscriber a message, including an invalid one to prove the JSON check works:

Send test messages to the worker
mosquitto_pub -h broker.connectmq.com -p 1883 -t 'testmqtt/php/sensors/room1' -m '{"temp":22.1}' -q 1
mosquitto_pub -h broker.connectmq.com -p 1883 -t 'testmqtt/php/sensors/room1' -m 'not json' -q 1

Finally, kill the worker with kill -9 and confirm the retained offline status appears on testmqtt/php/worker/status. More command-line recipes are in mosquitto_pub and mosquitto_sub examples.

Watch the PHP topics in the online client

php-mqtt/client makes MQTT in PHP straightforward once you respect PHP's process model: publish from requests with unique client IDs and a loop(true, true) for QoS 1, subscribe only from supervised CLI workers that reconnect on their own, and turn on TLS and credentials before you ship.

Frequently asked questions

Can PHP subscribe to MQTT topics?

Yes, but not inside a normal web request. A subscriber has to stay connected, so run it as a long-lived CLI script (php subscriber.php) that calls $mqtt->loop(true), supervised by systemd, Supervisor or Docker. Web requests should only publish.

Does php-mqtt/client support MQTT 5?

At the time of writing it implements MQTT 3.1 and 3.1.1, which every common broker accepts. If you need MQTT 5 features such as reason codes or user properties, handle that part of the system in another language or check the project for newer releases.

Why is my QoS 1 message from PHP never delivered?

publish() only queues QoS 1 and 2 messages; the acknowledgement is processed inside loop(). Call $mqtt->loop(true, true) after publishing so the client waits for the PUBACK before you disconnect, otherwise the script can exit first.