An MQTT latency test measures how long a message takes to travel from a publisher, through the broker, to a subscriber. The reliable way to do it is to measure round-trip time (RTT) on one clock: the same process publishes a message, receives it back from the broker and subtracts two readings of a monotonic timer. Anything that compares timestamps from two machines measures their clock difference as much as the network.
This guide gives you a copy-paste latency script, explains the clock pitfalls that ruin most measurements, shows how to report percentiles, and how to compare brokers or regions fairly.
What "MQTT latency" actually means
Several different numbers get called latency. Decide which one you need before writing any code:
| Measurement | From → to | Clock needed |
|---|---|---|
| Loopback RTT | Client publishes → same client receives its own message | One clock (easy, reliable) |
| Echo RTT | Client A → broker → echo client B → broker → client A | One clock on A |
| One-way latency | Publisher on host A → subscriber on host B | Two synchronized clocks |
| Connect time | TCP + TLS handshake + CONNECT → CONNACK | One clock |
| Network baseline | TCP connect to the broker port | One clock |
For capacity planning and broker comparisons, loopback RTT is usually enough. It includes the broker's routing work in both directions and two network traversals, and it needs nothing but a single client.
Clock pitfalls
Most bad latency numbers come from the clock, not the broker:
- Wall-clock time jumps.
time.time()in Python orDate.now()in JavaScript follow the system clock, which NTP may adjust at any moment. Use a monotonic clock for intervals:time.perf_counter()in Python,performance.now()orprocess.hrtime.bigint()in Node.js. - Two machines never agree exactly. Even with NTP, hosts are commonly off by a millisecond or more, and much more on devices without a good time source. A one-way measurement inherits that error directly — you can even get negative latencies.
- RTT / 2 is an estimate. Upload and download paths can differ (cellular, ADSL, congested uplinks). Report RTT as RTT, and only divide by two when you say so.
- Timer resolution on small devices.
millis()on a microcontroller has 1 ms resolution; average over many samples rather than trusting a single reading.
A loopback RTT script
This script (paho-mqtt 2.x) subscribes to a unique topic, publishes sequence numbers one at a time, and waits for each message to come back before sending the next. That closed-loop design measures the latency of an idle path without queuing effects.
import os, statistics, sys, threading, time, uuid
import paho.mqtt.client as mqtt
HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
N = int(sys.argv[1]) if len(sys.argv) > 1 else 500
QOS = int(os.getenv("MQTT_QOS", "1"))
WARMUP = 20
TOPIC = f"testmqtt/latency/{uuid.uuid4().hex[:8]}"
ready, got = threading.Event(), threading.Event()
state = {"expected": -1, "t0": 0.0}
samples, lost = [], 0
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe(TOPIC, qos=QOS)
def on_subscribe(client, userdata, mid, reason_code_list, properties):
ready.set()
def on_message(client, userdata, msg):
if int(msg.payload) == state["expected"]:
rtt = (time.perf_counter() - state["t0"]) * 1000
if state["expected"] >= WARMUP:
samples.append(rtt)
got.set()
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
if os.getenv("MQTT_USER"):
client.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
client.on_connect, client.on_subscribe, client.on_message = on_connect, on_subscribe, on_message
client.connect(HOST, PORT, keepalive=30)
client.loop_start()
ready.wait(10)
for i in range(N + WARMUP):
got.clear()
state["expected"], state["t0"] = i, time.perf_counter()
client.publish(TOPIC, str(i), qos=QOS)
if not got.wait(5):
lost += 1
time.sleep(0.02) # keep the path idle between probes
client.loop_stop()
q = statistics.quantiles(samples, n=100)
print(f"{HOST}:{PORT} QoS {QOS} samples {len(samples)} lost {lost}")
print(f"min {min(samples):.1f} p50 {q[49]:.1f} p95 {q[94]:.1f} "
f"p99 {q[98]:.1f} max {max(samples):.1f} ms")pip install "paho-mqtt>=2"
python mqtt_rtt.py 1000
MQTT_PORT=8883 python mqtt_rtt.py 1000 # needs client.tls_set() for TLS
MQTT_QOS=0 python mqtt_rtt.py 1000The first WARMUP messages are thrown away because they pay for cold caches and connection warm-up. If you need to test with TLS, add client.tls_set() before connect() and use port 8883 — the handshake happens once, so it affects connect time, not per-message RTT.
Before scripting, it is worth a quick sanity check in the online MQTT client: subscribe to testmqtt/latency/#, publish a message and confirm it comes back. If that works, the script will too.
Separate the network from the broker
An RTT of 40 ms tells you nothing until you know how much of it is the network. Measure the plain TCP connect time to the broker port as a baseline:
python3 -c "import socket,time
for _ in range(5):
t=time.perf_counter(); socket.create_connection(('broker.connectmq.com',1883)).close()
print(f'{(time.perf_counter()-t)*1000:.1f} ms')"A TCP connect is one network round trip. If your MQTT loopback RTT at QoS 0 is close to that baseline, the broker adds almost nothing and the remaining latency is distance. If it is much higher, look at the broker, the client or the path — high latency troubleshooting walks through the usual causes.
Light in fiber covers roughly 200 km per millisecond, so a broker 2,000 km away costs at least ~20 ms of RTT before any software runs. No broker tuning beats physics; choose a region close to your devices.
Report percentiles, not averages
Latency distributions have long tails. A mean of 12 ms can hide a p99 of 300 ms, and the p99 is what times out devices and makes dashboards stutter. Always report at least:
- min — the floor set by the network; it should be stable between runs.
- p50 — the typical experience.
- p95 / p99 — the tail; compare these between configurations.
- max and lost — single outliers and timeouts are signals, not noise.
Collect enough samples for the percentile you report. With 100 samples, p99 is effectively one message; aim for at least 1,000 samples before quoting p99.
What changes latency
| Factor | Effect on RTT |
|---|---|
| QoS 0 → QoS 1 | Delivery is not delayed by the PUBACK, but broker-side persistence and inflight limits can add time under load |
| QoS 2 | Four-packet handshake per hop; noticeably slower, see QoS 0 vs 1 vs 2 benchmark |
| TLS | Handshake cost on connect; small per-message encryption cost |
| WebSockets | Extra framing and often a proxy hop |
| Payload size | Larger payloads take longer on slow links |
| Broker load | Queues grow, tail latency rises first |
| Client callbacks | Slow on_message handlers delay every later message |
To see how latency behaves under load, run this script while a load generator pushes traffic — the method is covered in MQTT broker load testing. The idle RTT is your best case; the loaded RTT is what you plan around.
Comparing brokers and regions fairly
A comparison is only fair when everything except the broker is identical:
- Run from the same machine and network, ideally a VM in the region where your devices or backend live.
- Use the same QoS, payload, port and TLS setting for every target.
- Run each target interleaved (A, B, A, B) rather than all of A then all of B, so a temporary network hiccup affects both.
- Record min, p50, p99 and lost for each, plus the TCP baseline, in one table.
- Repeat at a different time of day before drawing conclusions.
For multi-region decisions, start a small VM in each region where you have users and run the same script against each broker endpoint. The resulting matrix of p50/p99 per (client region, broker region) is the most honest input you will get.
Latency test checklist
- Use a monotonic clock and a single process for RTT.
- Discard warm-up messages; measure connect time separately.
- Collect 1,000+ samples and report min, p50, p95, p99, max and lost.
- Record a TCP baseline next to every MQTT result.
- Test idle and under load, from the network your devices really use.
- Keep the script in your repository and rerun it after broker or network changes.
Check the round trip in your browser
A trustworthy MQTT latency test is mostly about discipline: one clock, enough samples, percentiles instead of averages, and a baseline that tells you how much of the number belongs to the network. Get those right and a 15-minute script answers questions that vendor benchmarks never will.
Frequently asked questions
How do I measure MQTT latency without synchronized clocks?
Measure round-trip time on a single clock: one client publishes to a topic it is also subscribed to (or to an echo client that republishes), and the same process timestamps the send and the receive with a monotonic clock.
Is MQTT round-trip time twice the one-way latency?
Only approximately. RTT / 2 assumes both directions are symmetric, which is often not true on cellular or asymmetric links. For real one-way numbers you need clocks synchronized well below the latency you are measuring.
Why is my first MQTT message much slower than the rest?
The first messages pay for DNS lookups, TCP and TLS warm-up, subscription setup and cold caches. Discard a warm-up batch before recording samples, and measure connection setup time separately.


