Tutorials

MQTT with curl: Publish and Subscribe from the Command Line

Use curl as an MQTT client: publish with -d, subscribe with mqtt:// URLs, encode wildcards, read the binary output and know when to switch to mosquitto_pub.

Updated · 7 min read

MQTT with curl: Publish and Subscribe from the Command Line

Yes, curl speaks MQTT. Since version 7.70.0 curl understands mqtt:// URLs: a GET (the default) subscribes to the topic in the URL path, and sending data with -d publishes to it. If curl is already on your server, CI runner or container, you can check that a broker accepts connections and routes a message without installing anything else.

The two commands you need
# Subscribe (blocks until you press Ctrl+C)
curl mqtt://broker.connectmq.com/testmqtt/curl/demo

# Publish (in a second terminal)
curl -d "hello from curl" mqtt://broker.connectmq.com/testmqtt/curl/demo

This guide covers how those commands behave, the exact output format, the limitations that matter, and when you should switch to a real MQTT client.

Check that your curl has MQTT

MQTT is a compile-time feature. Look for mqtt in the protocol list:

Is MQTT compiled in?
curl -V | grep -i protocols
# Protocols: dict file ftp ftps gopher ... mqtt pop3 pop3s rtsp ...

If mqtt is missing, curl answers an mqtt:// URL with curl: (1) Protocol "mqtt" not supported (older builds add "or disabled in libcurl"). Current releases of macOS, most Linux distributions and the official curlimages/curl Docker image include it; a minimal build may not.

No local curl with MQTT? Use the official image
docker run --rm curlimages/curl -d "hi" mqtt://broker.connectmq.com/testmqtt/curl/demo

How curl maps MQTT onto URLs

curl actionMQTT behaviour
curl mqtt://host/a/bCONNECT, SUBSCRIBE to a/b, print every message until interrupted
curl -d data mqtt://host/a/bCONNECT, PUBLISH data to a/b (QoS 0), DISCONNECT
mqtt://host:1884/...Non-default port (the default is 1883)
-u user:passUsername and password in the CONNECT packet
--max-time 10Stop a subscription after 10 seconds

The topic is the URL path without the leading slash, so mqtt://broker.connectmq.com/testmqtt/curl/demo means topic testmqtt/curl/demo. curl generates a random client ID that starts with curl (for example curlMsFB2wMW, visible with -v) and uses MQTT 3.1.1.

Publish messages

-d takes a string, or @file to read the payload from a file:

Publishing variants
# Plain text
curl -d "21.5" mqtt://broker.connectmq.com/testmqtt/curl/sensor/temp

# JSON from the shell
curl -d '{"temp":21.5,"unit":"C"}' mqtt://broker.connectmq.com/testmqtt/curl/sensor/json

# Payload from a file, bytes kept exactly as they are
curl --data-binary @reading.json mqtt://broker.connectmq.com/testmqtt/curl/sensor/json

# With credentials (your broker's username and password)
curl -u "$MQTT_USER:$MQTT_PASS" -d "on" mqtt://broker.connectmq.com/testmqtt/curl/relay/1

A successful publish prints nothing and exits with status 0, which makes curl handy in scripts and health checks. Keep in mind that exit code 0 only means the broker accepted the connection and curl sent the PUBLISH: at QoS 0 there is no acknowledgement, so it does not prove that anyone received the message.

Subscribe and read the output

A subscription runs until you press Ctrl+C or until a time limit stops it:

Subscribe for 30 seconds
curl -N --max-time 30 mqtt://broker.connectmq.com/testmqtt/curl/sensor/temp

-N (--no-buffer) makes curl write each message immediately instead of buffering output when you pipe it into another program. When the time limit is reached, curl exits with code 28 (operation timed out). That is the normal way a time-limited subscription ends, not an error.

Wildcards must be URL-encoded

+ can be written as is, but # starts a URL fragment and is cut off before curl sends anything. Encode it as %23:

Multi-level wildcard subscription
curl -N 'mqtt://broker.connectmq.com/testmqtt/curl/%23'          # testmqtt/curl/#
curl -N 'mqtt://broker.connectmq.com/testmqtt/curl/+/temp'       # single level

Not sure what a filter matches? Try it in the topic matcher or read MQTT topics and wildcards.

The output format

curl does not print messages as text lines. For every PUBLISH it receives, it writes:

  1. two bytes: the topic length (big-endian),
  2. the topic name,
  3. the payload, with no separator or trailing newline.

Here is a real capture of one message on a wildcard subscription:

xxd of curl's subscribe output
00000000: 0011 7465 7374 6d71 7474 2f63 7572 6c2f  ..testmqtt/curl/
00000010: 612f 627b 2274 223a 3231 2e35 7d         a/b{"t":21.5}

0x0011 is 17, the length of the topic testmqtt/curl/a/b, and the JSON payload follows directly. That is fine for "did anything arrive?", but it is awkward to parse when several messages arrive together. A little Python can split the stream:

curl-mqtt-split.py — usage: curl -sN mqtt://... | python3 curl-mqtt-split.py
import sys

# Splits curl's stream into topic/payload pairs.
# Works when payloads are small and arrive one per read; curl does not frame payload length.
buf = sys.stdin.buffer
while True:
    head = buf.read(2)
    if len(head) < 2:
        break
    n = int.from_bytes(head, "big")
    topic = buf.read(n).decode()
    payload = buf.read1(65536)   # whatever arrived with this message
    print(f"{topic}  {payload.decode(errors='replace')}", flush=True)

Because curl writes no payload length, no parser can be fully reliable. If you need to process messages programmatically, that alone is a good reason to use mosquitto_sub -v or a library.

Limitations you should know

Featurecurlmosquitto_pub / mosquitto_sub
MQTT version3.1.13.1, 3.1.1, 5
Publish QoS0 only0, 1, 2
Retained publishNo-r
TLS (mqtts, port 8883)No--cafile, -p 8883
WebSocketsNo (ws:// in curl is a plain WebSocket, not MQTT)No (use a browser client)
Custom client ID, keep-alive, Last WillNoYes
Readable subscribe outputBinary framing-v prints topic payload lines

The TLS gap is the important one. The curl manual says so directly: "MQTT over TLS is not supported (yet)." Never send production credentials with -u over plain port 1883 on an untrusted network: they travel in clear text. For anything beyond a smoke test against a plain-TCP listener, use mosquitto_pub and mosquitto_sub or MQTT CLI, both of which support TLS, MQTT 5 and every QoS level.

Errors and exit codes

What you seeMeaningFix
curl: (1) Protocol "mqtt" not supportedYour build has no MQTTUse another build or the curlimages/curl image
curl: (7) Failed to connect to ... port 1883Nothing is listening, or a firewall drops itCheck the host, port and firewall rules
curl: (8) Weird server replyThe broker refused the CONNECTRun with -v and read the CONNACK
Expected 0000 but got 0005 (with -v)CONNACK return code 5: not authorizedAdd or correct -u user:pass, check ACLs
curl: (28) Operation timed out--max-time was reachedExpected for time-limited subscriptions

With -v, the last two digits of Expected 0000 but got 00XX are the MQTT 3.1.1 CONNACK return code: 01 unacceptable protocol version, 02 identifier rejected, 03 server unavailable, 04 bad username or password, 05 not authorized. The MQTT reason code lookup explains each one, and CONNACK reason codes covers the causes in depth.

A curl smoke test for scripts and CI

curl's strengths are that it is everywhere and that it returns clean exit codes. This script subscribes in the background, publishes a unique token and checks that it came back:

curl-mqtt-smoke.sh
#!/usr/bin/env bash
set -u
HOST="${MQTT_HOST:-broker.connectmq.com}"
TOPIC="testmqtt/curl-smoke/$(date +%s)-$$"
TOKEN="ping-$RANDOM"
OUT="$(mktemp)"

curl -sN --max-time 5 "mqtt://$HOST/$TOPIC" > "$OUT" &
SUB=$!
sleep 1                                   # give the SUBSCRIBE time to land

if ! curl -s -d "$TOKEN" "mqtt://$HOST/$TOPIC"; then
  echo "FAIL: publish rejected (exit $?)"; exit 1
fi

wait "$SUB"                               # exits 28 when --max-time is reached
if grep -q "$TOKEN" "$OUT"; then
  echo "OK: message routed through $HOST"
else
  echo "FAIL: message not received"; exit 1
fi
rm -f "$OUT"

Run it from the machine or pipeline that needs broker access: a pass proves DNS, the TCP path, the CONNECT and routing in one go. For a fuller version with TLS and QoS checks, see the MQTT smoke test script and MQTT in Bash scripts.

To watch the messages your curl commands produce, open the online MQTT client and subscribe to the same topic. Seeing them arrive in the browser confirms both directions at once.

Watch curl's messages in the online client

curl is the fastest way to answer "can this machine reach the broker and publish?" without installing anything. Its MQTT support is basic on purpose: QoS 0, no retain, no TLS and binary-framed output. Use it for smoke tests and quick checks, and switch to mosquitto_pub, MQTT CLI or a client library once you need security, reliability or readable output.

Frequently asked questions

Does curl support MQTT over TLS (mqtts://)?

Not in the curl versions shipped by most systems: the curl manual states that MQTT over TLS is not supported. Use curl only against plain TCP port 1883 for quick checks, and use mosquitto_pub, MQTT CLI or a library for TLS on port 8883.

Can curl publish retained or QoS 1 messages?

No. curl publishes with QoS 0 and the retain flag cleared, and there is no option to change either. If you need retained messages, QoS 1/2 or MQTT 5 properties, use mosquitto_pub or the online client.

Why does curl print strange characters before each MQTT message?

When subscribing, curl writes every PUBLISH it receives as a 2-byte topic length, the topic name and then the payload, with no separator. The first two bytes are binary, so they show up as garbage in a terminal. Pipe the output through xxd or a small parser to read it.