emqtt-bench (the binary is called emqtt_bench) is an open-source MQTT benchmarking tool from EMQ, written in Erlang, that can open tens of thousands of connections and drive publish and subscribe load from a single machine. It has three commands — conn, sub and pub — and a long list of flags. This guide explains each command, the flags that actually matter, TLS and WebSocket usage, latency measurement and how to read what it prints.
For the broader method — what to measure and how to plan a benchmark — see MQTT broker load testing. This page is the reference for the tool itself.
Install or run with Docker
The quickest way is the Docker image, whose entrypoint is emqtt_bench:
docker run --rm -it emqx/emqtt-bench pub --helpFor long or very large runs, a native install avoids Docker networking overhead. Prebuilt packages are published on the project's GitHub releases page. Building from source needs a recent Erlang/OTP and libatomic:
git clone https://github.com/emqx/emqtt-bench.git
cd emqtt-bench
make # BUILD_WITHOUT_QUIC=1 make if the QUIC build fails
./emqtt_bench conn --helpBefore any benchmark, check the host, port and credentials by hand with one client in the online MQTT client. If one connection fails, two thousand will fail faster. The examples use broker.connectmq.com, a shared endpoint suited to quick checks; run real benchmarks against your own private broker instance by changing -h and the credentials.
Options shared by all commands
| Flag | Meaning | Default |
|---|---|---|
-h, --host | Broker host; a comma-separated list spreads clients over several hosts | localhost |
-p, --port | Broker port | 1883 |
-V, --version | MQTT protocol version: 3 (3.1), 4 (3.1.1) or 5 | 5 |
-c, --count | Number of clients to start | 200 |
-i, --interval | Milliseconds between new connections | 10 |
-R, --connrate | Connections per second; 0 falls back to -i | 0 |
-u, -P | Username and password | none |
-k, --keepalive | Keep alive in seconds | 300 |
-C, --clean | Clean session / clean start | true |
-x, --session-expiry | MQTT 5 session expiry interval in seconds | 0 |
--prefix | Client ID prefix (no random suffix is added when set) | <host>_bench_<cmd>_<random>_<n> |
--shortids | Short client IDs: the sequence number, or prefix + number | false |
-n, --startnumber | First sequence number; avoids ID clashes between several bench instances | 0 |
--ifaddr | Local IP address(es) to bind, comma-separated | system default |
--reconnect | Reconnect attempts after a disconnect | 0 (off) |
--num-retry-connect | Retries for the initial connect | 0 |
-l, --lowmem | Low-memory mode: less RAM per client, more CPU | off |
Two details trip people up. First, the default protocol version is MQTT 5; pass -V 4 to benchmark a broker or configuration that only speaks 3.1.1. Second, the keep alive default of 300 s is much longer than most devices use; set -k to your fleet's value if you are testing idle-connection behaviour.
Client IDs must be unique per broker. When you run several emqtt-bench processes against the same broker, give each its own --prefix or -n range — duplicate IDs cause session takeovers that look like random disconnects. You can preview ID formats with the client ID generator.
conn: connection capacity
conn only connects and holds the connections. It answers "how many concurrent clients can the broker hold, and how fast can it accept them?"
docker run --rm -it emqx/emqtt-bench conn \
-h broker.connectmq.com -p 1883 \
-c 5000 -R 500 -k 60 \
-u "$MQTT_USER" -P "$MQTT_PASS"Watch connect_succ climb to the target and connect_fail stay at zero. A reconnect storm — every device coming back after an outage — is simulated by raising -R. More scenarios are in the connection stress test guide.
sub: subscribers
sub connects clients and subscribes each one to a topic, then counts what it receives.
| Flag | Meaning | Default |
|---|---|---|
-t, --topic | Topic filter, with placeholders (see below) | required |
-q, --qos | Subscription QoS | 0 |
--payload-hdrs | Parse headers the publisher added (ts, cnt64) | none |
docker run --rm -it emqx/emqtt-bench sub \
-h broker.connectmq.com -p 1883 \
-c 100 -i 5 -q 1 \
-t 'testmqtt/bench/%i/data' \
-u "$MQTT_USER" -P "$MQTT_PASS"pub: publishers
pub connects clients and publishes at a fixed interval per client.
| Flag | Meaning | Default |
|---|---|---|
-t, --topic | Topic to publish to, with placeholders | required |
-I, --interval_of_msg | Milliseconds between messages, per client | 1000 |
-s, --size | Payload size in bytes | 256 |
-m, --message | Literal payload, or template://<file> | random bytes |
-q, --qos | Publish QoS | 0 |
-r, --retain | Set the retain flag | false |
-L, --limit | Stop after this many messages; 0 = unlimited | 0 |
-F, --inflight | Max unacknowledged QoS 1/2 messages per client; 0 = unlimited | 1 |
-w, --wait-before-publishing | Start publishing only after all publishers tried to connect | false |
--payload-hdrs | Prefix payloads with ts (timestamp) and/or cnt64 (counter) | none |
docker run --rm -it emqx/emqtt-bench pub \
-h broker.connectmq.com -p 1883 \
-c 100 -I 100 -s 512 -q 1 -F 10 -w \
-t 'testmqtt/bench/%i/data' \
-u "$MQTT_USER" -P "$MQTT_PASS"The target rate is clients × (1000 / -I). Note -F: with the default inflight window of 1, each QoS 1 client waits for every PUBACK before sending the next message, so at high latency a client can never reach its interval. If you see pub_overrun counting up, the client could not keep its schedule; raise -F or lower the rate. Throughput testing strategies are covered in the MQTT throughput test guide.
Topic placeholders
Placeholders in -t let one command create many distinct topics:
| Placeholder | Expands to |
|---|---|
%i | Client sequence number (includes the -n offset) |
%c | Client ID |
%u | The username given with -u |
%s | Message sequence number (pub only) |
%rand_N | Random integer from 1 to N |
Using %i on both sides — pub -t 'testmqtt/bench/%i/data' and sub -t 'testmqtt/bench/%i/data' with the same -c — pairs publisher N with subscriber N: one-to-one routing. A single subscriber on testmqtt/bench/+/data instead measures fan-in, and many subscribers on one topic measure fan-out. Check your filters with the topic matcher.
TLS, WebSockets and QUIC
| Flag | Meaning |
|---|---|
-S, --ssl | Enable TLS |
--ssl-version | Force a TLS version (implies --ssl) |
--cacertfile | CA bundle used to verify the broker |
--certfile, --keyfile | Client certificate and key (mutual TLS) |
--ciphers | Restrict cipher suites |
--ws | Use WebSocket transport |
--quic | Use MQTT over QUIC (brokers that support it) |
# MQTT over TLS
emqtt_bench conn -h broker.connectmq.com -p 8883 --ssl \
--cacertfile /etc/ssl/certs/ca-certificates.crt \
-c 1000 -R 100 -u "$MQTT_USER" -P "$MQTT_PASS"
# MQTT over secure WebSocket
emqtt_bench pub -h broker.connectmq.com -p 8084 --ws --ssl \
-c 50 -I 1000 -t 'testmqtt/bench/%i/ws' -u "$MQTT_USER" -P "$MQTT_PASS"emqtt-bench has no separate WebSocket path flag; the underlying emqtt client uses /mqtt, which matches the endpoint used throughout these examples. Comparing the same conn run over 1883 and 8883 shows the handshake cost directly — see MQTT TLS performance testing.
Measuring latency
Start the subscriber and the publisher with the same --payload-hdrs ts. The publisher writes a millisecond timestamp at the start of every payload; the subscriber subtracts it from its own clock on arrival:
# terminal 1
emqtt_bench sub -h broker.connectmq.com -c 50 -q 1 \
-t 'testmqtt/bench/%i/lat' --payload-hdrs ts -u "$MQTT_USER" -P "$MQTT_PASS"
# terminal 2
emqtt_bench pub -h broker.connectmq.com -c 50 -I 200 -q 1 \
-t 'testmqtt/bench/%i/lat' --payload-hdrs ts -u "$MQTT_USER" -P "$MQTT_PASS"The subscriber then prints a publish_latency avg=…ms line. It is an average; for percentiles enable --prometheus (exposes an e2e_latency histogram) or use the script in the load testing guide. Because the timestamp comes from the publisher's clock, run both on one machine or keep the machines' clocks synchronized.
Reading the output
Every second, emqtt-bench prints one line per counter that changed, in the form <elapsed> <counter> total=<n> rate=<n>/sec:
10s connect_succ total=100 rate=0.00/sec
10s pub total=9871 rate=1000.12/sec
10s pub_succ total=9862 rate=999.40/sec| Counter | Meaning |
|---|---|
connect_succ / connect_fail | Connections accepted / failed |
connect_retried, reconnect_succ | Retries and successful reconnects |
connection_refused, connection_timeout, unreachable | Why connections failed |
connection_idle | Connections that went idle |
sub / sub_fail | Subscriptions accepted / rejected |
pub / pub_succ / pub_fail | Messages sent / acknowledged / failed |
pub_overrun | A client could not keep its publish interval |
pub_nosub | Broker reported no matching subscribers (MQTT 5) |
recv | Messages received by subscribers |
publish_latency | Average end-to-end latency (with --payload-hdrs ts) |
The healthy pattern: pub and recv rates match your target and each other, pub_overrun stays at zero, and no failure counters appear. A rising connection_timeout or connect_fail near a round number (1,024, 65,535) usually points to limits on the load generator. Look up any broker reason codes with the reason code tool.
Scaling the load generator
- File descriptors. Raise
ulimit -nwell above-cbefore largeconnruns. - Ports. One source IP can open only about 64K connections to one broker address and port. Use
--ifaddr 10.0.0.11,10.0.0.12or several machines, and widen the local port range withsysctl. - Memory. Add
-lfor very large client counts at the cost of more CPU. - CPU. If the generator is above roughly 80 % CPU, your numbers describe the generator, not the broker.
Watch benchmark traffic in the online client
emqtt-bench is the fastest way to put realistic connection and message load on an MQTT broker from the command line. Learn the handful of flags that shape the load — -c, -R, -I, -q, -F and the topic placeholders — confirm TLS and protocol versions match production, and read the counters as rates you can compare from run to run.
Frequently asked questions
What is the difference between -i and -R in emqtt-bench?
-i (--interval) is the pause in milliseconds between new connections, default 10. -R (--connrate) sets a connection rate per second instead; when it is 0 (the default), emqtt-bench falls back to the interval.
How do I measure latency with emqtt-bench?
Start the subscriber and the publisher with the same --payload-hdrs ts option. The publisher prefixes each payload with a millisecond timestamp and the subscriber prints a publish_latency line with the average end-to-end latency. Run both on one machine or keep clocks in sync.
Why does emqtt-bench stop at around 64,000 connections?
One source IP address can only open about 64K TCP connections to the same broker address and port. Spread the clients over several local addresses with --ifaddr, or run emqtt-bench on several machines, and raise ulimit -n.



