Broker Testing

emqtt-bench Guide: Every Command and Flag Explained

A complete emqtt-bench guide: conn, pub and sub modes, every important flag, TLS and WebSocket options, latency headers and how to read the output counters.

Updated · 10 min read

emqtt-bench Guide: Every Command and Flag Explained

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:

Run with Docker
docker run --rm -it emqx/emqtt-bench pub --help

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

Build from source
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 --help

Before 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

FlagMeaningDefault
-h, --hostBroker host; a comma-separated list spreads clients over several hostslocalhost
-p, --portBroker port1883
-V, --versionMQTT protocol version: 3 (3.1), 4 (3.1.1) or 55
-c, --countNumber of clients to start200
-i, --intervalMilliseconds between new connections10
-R, --connrateConnections per second; 0 falls back to -i0
-u, -PUsername and passwordnone
-k, --keepaliveKeep alive in seconds300
-C, --cleanClean session / clean starttrue
-x, --session-expiryMQTT 5 session expiry interval in seconds0
--prefixClient ID prefix (no random suffix is added when set)<host>_bench_<cmd>_<random>_<n>
--shortidsShort client IDs: the sequence number, or prefix + numberfalse
-n, --startnumberFirst sequence number; avoids ID clashes between several bench instances0
--ifaddrLocal IP address(es) to bind, comma-separatedsystem default
--reconnectReconnect attempts after a disconnect0 (off)
--num-retry-connectRetries for the initial connect0
-l, --lowmemLow-memory mode: less RAM per client, more CPUoff

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?"

5,000 clients at 500 connections per second
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.

FlagMeaningDefault
-t, --topicTopic filter, with placeholders (see below)required
-q, --qosSubscription QoS0
--payload-hdrsParse headers the publisher added (ts, cnt64)none
100 subscribers, one topic each
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.

FlagMeaningDefault
-t, --topicTopic to publish to, with placeholdersrequired
-I, --interval_of_msgMilliseconds between messages, per client1000
-s, --sizePayload size in bytes256
-m, --messageLiteral payload, or template://<file>random bytes
-q, --qosPublish QoS0
-r, --retainSet the retain flagfalse
-L, --limitStop after this many messages; 0 = unlimited0
-F, --inflightMax unacknowledged QoS 1/2 messages per client; 0 = unlimited1
-w, --wait-before-publishingStart publishing only after all publishers tried to connectfalse
--payload-hdrsPrefix payloads with ts (timestamp) and/or cnt64 (counter)none
100 publishers, 10 msg/s each = 1,000 msg/s
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:

PlaceholderExpands to
%iClient sequence number (includes the -n offset)
%cClient ID
%uThe username given with -u
%sMessage sequence number (pub only)
%rand_NRandom 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

FlagMeaning
-S, --sslEnable TLS
--ssl-versionForce a TLS version (implies --ssl)
--cacertfileCA bundle used to verify the broker
--certfile, --keyfileClient certificate and key (mutual TLS)
--ciphersRestrict cipher suites
--wsUse WebSocket transport
--quicUse MQTT over QUIC (brokers that support it)
TLS and secure WebSocket
# 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:

End-to-end latency
# 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:

Typical pub output
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
CounterMeaning
connect_succ / connect_failConnections accepted / failed
connect_retried, reconnect_succRetries and successful reconnects
connection_refused, connection_timeout, unreachableWhy connections failed
connection_idleConnections that went idle
sub / sub_failSubscriptions accepted / rejected
pub / pub_succ / pub_failMessages sent / acknowledged / failed
pub_overrunA client could not keep its publish interval
pub_nosubBroker reported no matching subscribers (MQTT 5)
recvMessages received by subscribers
publish_latencyAverage 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 -n well above -c before large conn runs.
  • 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.12 or several machines, and widen the local port range with sysctl.
  • Memory. Add -l for 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.