Broker Testing

MQTT Latency Test: Measure Round-Trip Time Correctly

How to run an MQTT latency test: measure publish-to-receive round-trip time with one clock, avoid NTP pitfalls, report p50/p99 and compare broker regions.

Updated · 8 min read

MQTT Latency Test: Measure Round-Trip Time Correctly

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:

MeasurementFrom → toClock needed
Loopback RTTClient publishes → same client receives its own messageOne clock (easy, reliable)
Echo RTTClient A → broker → echo client B → broker → client AOne clock on A
One-way latencyPublisher on host A → subscriber on host BTwo synchronized clocks
Connect timeTCP + TLS handshake + CONNECT → CONNACKOne clock
Network baselineTCP connect to the broker portOne 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 or Date.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() or process.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.

mqtt_rtt.py
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")
Run it
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 1000

The 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:

TCP baseline (includes DNS on the first call)
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

FactorEffect on RTT
QoS 0 → QoS 1Delivery is not delayed by the PUBACK, but broker-side persistence and inflight limits can add time under load
QoS 2Four-packet handshake per hop; noticeably slower, see QoS 0 vs 1 vs 2 benchmark
TLSHandshake cost on connect; small per-message encryption cost
WebSocketsExtra framing and often a proxy hop
Payload sizeLarger payloads take longer on slow links
Broker loadQueues grow, tail latency rises first
Client callbacksSlow 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:

  1. Run from the same machine and network, ideally a VM in the region where your devices or backend live.
  2. Use the same QoS, payload, port and TLS setting for every target.
  3. Run each target interleaved (A, B, A, B) rather than all of A then all of B, so a temporary network hiccup affects both.
  4. Record min, p50, p99 and lost for each, plus the TCP baseline, in one table.
  5. 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.