IoT Tutorials

PHP MQTT Client Tutorial with php-mqtt/client

Learn to publish MQTT messages from a PHP web request and run a long-lived PHP subscriber worker with php-mqtt/client, including TLS, reconnects, graceful shutdown and Laravel-friendly patterns.

PHP MQTT client diagram with a web app publishing commands and a background worker subscribing to IoT topics on a cloud 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

A PHP MQTT client lets your existing web application send commands to devices and react to their data without adding a new language to your stack. In this tutorial you will use the php-mqtt/client package to publish from a web request, run a long-lived subscriber worker, secure both with TLS, and keep the worker alive in production.

Two Very Different Jobs for a PHP MQTT Client

PHP's request model shapes how you use MQTT, so separate the two cases from the start:

Job Lifetime Pattern
Publish a command when a user clicks a button One HTTP request Connect, publish, disconnect
React to every message devices send Forever Separate CLI worker with a blocking loop

Trying to subscribe inside a web request is the most common mistake. PHP-FPM workers are meant to finish in milliseconds; a subscription needs a process that stays connected for days. Keep subscribers in a CLI process supervised by systemd, Supervisor or your container runtime, and keep web requests to short publishes.

Installing php-mqtt/client

php-mqtt/client is a pure PHP implementation of MQTT 3.1 and 3.1.1 with TLS support, maintained on GitHub and installed through Composer. It needs no PHP extension beyond the standard sockets and OpenSSL support most builds include.

composer require php-mqtt/client

The package README is the official php-mqtt/client documentation and lists every connection setting. For a comparison with clients in other languages, see the MQTT client libraries guide.

Shared Connection Settings

Put connection details in one place. The example targets the public MQTT test broker at broker.connectmq.com over TLS on port 8883, and switches to private credentials automatically when environment variables are set.

mqtt.php:

<?php

require __DIR__ . '/vendor/autoload.php';

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

const MQTT_HOST = 'broker.connectmq.com';
const MQTT_PORT = 8883;

function mqtt_topic_prefix(): string
{
    return getenv('MQTT_TOPIC_PREFIX') ?: 'connectmq/demo/php-5e19c2';
}

function mqtt_client(string $role): MqttClient
{
    $clientId = getenv('MQTT_CLIENT_ID') ?: sprintf('php-%s-%s', $role, bin2hex(random_bytes(4)));
    return new MqttClient(MQTT_HOST, MQTT_PORT, $clientId, MqttClient::MQTT_3_1_1);
}

function mqtt_settings(bool $reconnect = false): ConnectionSettings
{
    $settings = (new ConnectionSettings())
        ->setUseTls(true)
        ->setTlsVerifyPeer(true)
        ->setTlsVerifyPeerName(true)
        ->setConnectTimeout(10)
        ->setSocketTimeout(5)
        ->setKeepAliveInterval(30);

    if ($user = getenv('MQTT_USERNAME')) {
        $settings = $settings
            ->setUsername($user)
            ->setPassword(getenv('MQTT_PASSWORD') ?: null);
    }

    if ($reconnect) {
        $settings = $settings
            ->setReconnectAutomatically(true)
            ->setMaxReconnectAttempts(10)
            ->setDelayBetweenReconnectAttempts(3000);
    }

    return $settings;
}

ConnectionSettings is immutable: every setter returns a new instance, which is why the code reassigns $settings. TLS verification uses the system CA store, which already trusts the Let's Encrypt certificate ConnectMQ serves.

Publishing From a Web Request

This endpoint receives a form post and publishes a command to a device. With QoS 1 the broker must acknowledge the message, and php-mqtt/client only processes that acknowledgement inside its loop, so the code runs the loop until the outgoing queue is empty before disconnecting.

public/command.php:

<?php

require __DIR__ . '/../mqtt.php';

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

$device = preg_replace('/[^a-z0-9_-]/i', '', $_POST['device'] ?? '');
$action = in_array($_POST['action'] ?? '', ['on', 'off'], true) ? $_POST['action'] : null;

if ($device === '' || $action === null) {
    http_response_code(400);
    exit('Invalid device or action');
}

$topic = mqtt_topic_prefix() . "/devices/{$device}/set";
$payload = json_encode(['action' => $action, 'ts' => time()]);

try {
    $mqtt = mqtt_client('web');
    $mqtt->connect(mqtt_settings(), true);
    $mqtt->publish($topic, $payload, MqttClient::QOS_AT_LEAST_ONCE);
    $mqtt->loop(true, true);   // wait until the PUBACK has been received
    $mqtt->disconnect();
    echo "Sent {$action} to {$device}";
} catch (MqttClientException $e) {
    error_log('MQTT publish failed: ' . $e->getMessage());
    http_response_code(502);
    echo 'Device gateway unavailable';
}

Notice the input sanitising on device. Users must never be able to inject #, + or / into a topic name, or they can address devices that are not theirs.

A TLS connection per request adds latency, typically a handshake round trip or two. For an admin panel that sends a few commands per minute this is fine. For high-volume publishing from PHP, push work onto a queue (Redis, a database table, Laravel queues) and let a long-lived worker hold one MQTT connection and drain it.

A Long-Lived PHP MQTT Subscriber

The subscriber is a CLI script. It subscribes with a wildcard, decodes JSON, and stops cleanly on SIGTERM so systemd or Docker can restart it without dropping a message mid-processing.

bin/subscriber.php:

<?php

require __DIR__ . '/../mqtt.php';

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

pcntl_async_signals(true);

$mqtt = mqtt_client('worker');

$stop = function () use ($mqtt) {
    fwrite(STDOUT, "Shutting down...\n");
    $mqtt->interrupt();
};
pcntl_signal(SIGINT, $stop);
pcntl_signal(SIGTERM, $stop);

$filter = mqtt_topic_prefix() . '/devices/+/telemetry';

try {
    $mqtt->connect(mqtt_settings(reconnect: true), true);

    $mqtt->subscribe($filter, function (string $topic, string $message, bool $retained) {
        $parts = explode('/', $topic);
        $device = $parts[count($parts) - 2] ?? 'unknown';
        $data = json_decode($message, true);

        if (!is_array($data)) {
            fwrite(STDERR, "Ignoring non-JSON payload on {$topic}\n");
            return;
        }

        // Replace with your own persistence, e.g. PDO insert or a framework model
        printf("[%s] %s%s %s\n", date('c'), $device, $retained ? ' (retained)' : '', json_encode($data));
    }, MqttClient::QOS_AT_LEAST_ONCE);

    fwrite(STDOUT, "Subscribed to {$filter}\n");
    $mqtt->loop(true);
    $mqtt->disconnect();
} catch (MqttClientException $e) {
    fwrite(STDERR, 'MQTT error: ' . $e->getMessage() . "\n");
    exit(1);
}

Run it:

php bin/subscriber.php

In another terminal, act as a device:

mosquitto_pub -h broker.connectmq.com -p 1883 -i php-sim-pump1 \
  -t "connectmq/demo/php-5e19c2/devices/pump1/telemetry" \
  -m '{"flow": 12.4, "pressure": 2.1}'

The pcntl extension is available in the PHP CLI on Linux but usually not under PHP-FPM or on Windows, which is one more reason to keep the subscriber a CLI process.

Running the Worker in Production

/etc/systemd/system/php-mqtt-worker.service:

[Unit]
Description=PHP MQTT subscriber
After=network-online.target
Wants=network-online.target

[Service]
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php bin/subscriber.php
EnvironmentFile=/etc/php-mqtt-worker.env
Restart=always
RestartSec=5
KillSignal=SIGTERM

[Install]
WantedBy=multi-user.target

/etc/php-mqtt-worker.env (mode 600) holds the private broker credentials:

MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
MQTT_TOPIC_PREFIX=acme/site1

Long-running PHP processes have their own failure modes. Work through this checklist before you rely on the worker:

  1. Memory. Unset large arrays, avoid accumulating data in static properties, and consider exiting after a few hours so systemd restarts a clean process.
  2. Database connections. PDO connections time out while the worker is idle; reconnect on failure instead of holding one connection forever.
  3. Unique client IDs. The web publisher and the worker need different client IDs. If two processes share one, the broker disconnects the older one, and they kick each other off in a loop.
  4. Reconnect limits. With setMaxReconnectAttempts(10) the loop eventually throws; let it exit with a non-zero code so the supervisor restarts it.
  5. Persistent sessions. If the worker must not miss messages during restarts, connect with clean session off and a fixed client ID, and subscribe at QoS 1. The persistent session guide explains the trade-off.

On a private ConnectMQ broker, client IDs are bound to the credentials you are issued, so plan for one credential set per process that connects at the same time.

Choosing QoS, Retain and Last Will From PHP

For commands sent from a web app, QoS 1 is the sensible default: the broker acknowledges receipt, and the device should report the resulting state on its own topic so the UI shows what really happened. Avoid the retain flag on commands. A retained on would be replayed to the device every time it reconnects, possibly hours later; retain is for state, not for actions, as the guide to MQTT retained messages explains.

The subscriber worker can announce its own health with a last will. Add ->setLastWillTopic(mqtt_topic_prefix() . '/workers/php/status'), ->setLastWillMessage('offline'), ->setLastWillQualityOfService(1) and ->setRetainLastWill(true) to the worker's connection settings, then publish a retained online right after connecting. If the worker crashes or loses its network, the broker publishes offline for it, and your monitoring can alert on that topic instead of guessing from silence. The free ConnectMQ Serverless plan supports QoS 0 and 1 and messages up to 256 KB, which comfortably covers command and telemetry payloads from a PHP application.

Laravel and Symfony Notes

The same code drops into frameworks. In Laravel, wrap the subscriber in an Artisan command (php artisan mqtt:listen) and run it under Supervisor, and publish from a queued job rather than from the controller. In Symfony, a console command plays the same role. The community php-mqtt/laravel-client package adds configuration and a facade on top of the same library if you prefer that style.

Troubleshooting

Error Likely cause
ConnectingToBrokerFailedException with a TLS error Wrong port for TLS (use 8883), or outdated CA bundle on the server
Connection refused with bad credentials MQTT_USERNAME or MQTT_PASSWORD missing in the worker's environment
Publish returns but device never receives it Script exited before the loop processed the QoS 1 acknowledgement
Worker reconnects every few seconds Another process uses the same client ID
Call to undefined function pcntl_signal Running under FPM or without the pcntl extension

Get Started with ConnectMQ

Give your PHP application a private, secure broker in a few minutes. Create a free Serverless broker at app.connectmq.com with no credit card, put the generated credentials in your environment file, and use the portal's in-browser client to test your publishes. A managed MQTT broker means TLS, scaling and broker upgrades stay off your PHP team's plate. For other back-end languages, see the Ruby MQTT client tutorial and the Paho MQTT C tutorial.

Frequently Asked Questions

Does php-mqtt/client support MQTT 5?

No. It supports MQTT 3.1 and 3.1.1, which ConnectMQ accepts alongside MQTT 5.0. For most web-to-device command flows, 3.1.1 is sufficient.

Can I subscribe to MQTT topics inside a PHP web request?

You can, but you should not. Subscriptions need a process that stays connected, so run them in a CLI worker and keep web requests to short publishes.

Why does my QoS 1 publish from PHP sometimes not arrive?

The script probably exited before the client processed the broker's acknowledgement. Call $mqtt->loop(true, true) after publishing so the queue drains before disconnect().

How do I connect PHP to MQTT over TLS?

Enable setUseTls(true) on the connection settings and connect to port 8883. Peer verification works out of the box against brokers with publicly trusted certificates such as broker.connectmq.com.

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.