An MQTT connection stress test answers a simple question: how many clients can this broker hold at once, and how fast can it accept them? The method is to ramp up idle connections at a controlled rate while you watch broker memory, CPU and connect latency — and to make sure the limit you hit belongs to the broker, not to the machine generating the load.
Most first attempts stop at a suspiciously round number. This guide shows how to remove those client-side limits, run the test with emqtt-bench or a Node.js script, and simulate the reconnect storm that follows every broker restart.
What you are actually measuring
| Metric | Why it matters |
|---|---|
| Max concurrent connections | Fleet size the broker can hold idle |
| Connection rate (CONNECT/s) | How quickly a fleet can come back after an outage |
| Connect latency (p50/p99) | Time from TCP SYN to CONNACK under load |
| Broker memory per connection | Capacity planning; measure, don't guess |
| Error types | Refused, timed out, CONNACK reason codes |
Connections cost the broker memory for socket buffers, session state and subscriptions, plus CPU for TLS handshakes and keep-alive processing. All of these depend on broker, version and configuration, so the only meaningful number is the one you measure with your settings: same TLS, same keep-alive, same authentication and roughly the same subscriptions per client.
Fix the load generator first
A single Linux machine has three limits that cap outgoing connections long before a decent broker gives up.
File descriptors
Every TCP socket is a file descriptor. The default soft limit is often 1,024:
ulimit -n # current soft limit
ulimit -n 200000 # raise for this shell (up to the hard limit)
cat /proc/sys/fs/file-max # system-wide ceilingFor a permanent change, set nofile in /etc/security/limits.conf or, for Docker, pass --ulimit nofile=200000:200000.
Ephemeral ports
Each connection from one source IP to one broker IP and port needs a unique local port. The default Linux range is 32768–60999, about 28,000 ports — the classic place where tests plateau.
sysctl net.ipv4.ip_local_port_range
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65000"That gives you about 64,000 connections per source IP. Beyond that you need more source IPs (or more broker ports/IPs, or more load machines).
Connection tracking
If the load generator runs a firewall with connection tracking (common with Docker and iptables), the conntrack table can fill up. The kernel log then shows nf_conntrack: table full, dropping packet.
sudo sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
sudo sysctl -w net.netfilter.nf_conntrack_max=262144Running the test with emqtt-bench
emqtt-bench is built for this. The conn command opens connections at a fixed rate and keeps them alive:
docker run --rm -it --ulimit nofile=200000:200000 emqx/emqtt-bench conn \
-h "$MQTT_HOST" -p 1883 \
-c 10000 -R 500 -k 60 \
-u "$MQTT_USER" -P "$MQTT_PASS"Set MQTT_HOST to your own broker; the shared broker.connectmq.com endpoint is only for small smoke runs. -c is the number of clients, -R the connection rate per second and -k the keep-alive in seconds. Use the same keep-alive your devices use: shorter keep-alives mean more PINGREQ traffic per idle client.
To go past one source IP, add secondary addresses to the load machine and let emqtt-bench spread the clients across them with --ifaddr. Use host networking so the container sees those addresses:
docker run --rm -it --network host --ulimit nofile=200000:200000 \
emqx/emqtt-bench conn \
-h "$MQTT_HOST" -p 1883 \
-c 50000 -R 1000 -k 60 \
--ifaddr 10.0.0.11,10.0.0.12Ramp in steps — for example 10k, 25k, 50k — and hold each level for several minutes. A broker that accepts 50,000 connections but starts dropping them during the next keep-alive cycle has not passed.
A Node.js script you can modify
When you need real authentication flows, per-client subscriptions or custom client IDs, a small MQTT.js 5 script is easier to adapt than a benchmark tool:
import mqtt from "mqtt";
const URL = process.env.MQTT_URL ?? "mqtt://broker.connectmq.com:1883";
const TOTAL = Number(process.argv[2] ?? 100); // default: a small smoke run
const RATE = Number(process.argv[3] ?? 200); // new connections per second
let opened = 0, up = 0, errors = 0, closes = 0;
function open(i) {
const c = mqtt.connect(URL, {
clientId: `stress-${process.pid}-${i}`,
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
keepalive: 60,
connectTimeout: 15_000,
reconnectPeriod: 2000 + Math.floor(Math.random() * 8000), // jitter per client
});
let online = false;
c.on("connect", () => {
if (!online) { online = true; up++; }
c.subscribe(`testmqtt/stress/${i}/cmd`, { qos: 1 });
});
c.on("close", () => { if (online) { online = false; up--; } closes++; });
c.on("error", () => errors++);
}
const timer = setInterval(() => {
for (let k = 0; k < RATE / 10 && opened < TOTAL; k++) open(opened++);
if (opened >= TOTAL) clearInterval(timer);
}, 100);
setInterval(() => {
console.log(`${new Date().toISOString()} opened=${opened} connected=${up} closes=${closes} errors=${errors}`);
}, 1000);npm install mqtt@5
ulimit -n 100000
MQTT_URL="mqtts://$MQTT_HOST:8883" node conn-stress.mjs 20000 300One Node.js process can comfortably hold several thousand idle connections; for more, run several processes or machines. Watch the load generator's CPU — if it is pegged, your numbers measure the client, not the broker. To check the broker's answer to rejected connections, look the CONNACK code up in the reason code reference.
Watch the broker side
Client-side counters only tell half the story. During every step, record on the broker host:
ss -s # socket summary
ss -tn state established '( sport = :1883 )' | wc -l
ps -o rss=,pcpu= -p "$(pgrep -d, mosquitto)" # memory (KB) and CPU of the broker processMosquitto also publishes $SYS/broker/clients/connected; subscribe to it with the online client or mosquitto_sub -t '$SYS/broker/clients/connected' -v — see MQTT $SYS topics. Remember that the broker host needs the same file-descriptor headroom: for Mosquitto under systemd, set LimitNOFILE= in a drop-in for the service, and check max_connections in mosquitto.conf (the Mosquitto config generator can produce a baseline). Errors that appear only in broker logs are covered in too many connections.
Memory per connection is easy to derive: (RSS at N connections − RSS idle) ÷ N. Measure it with and without TLS and with your real subscription count, because both change the result.
Simulate a reconnect storm
Holding 50,000 idle clients is the easy part. The dangerous moment is when they all reconnect at once — after a broker restart, a load balancer failover or a regional network blip. That is a test of connection rate, TLS handshake throughput and authentication backend capacity at the same time.
Two ways to trigger it on your own broker:
- Restart the broker while the stress script is holding connections, and watch
connected=climb back. - Cut the network between load generator and broker for longer than the keep-alive (for example with an iptables rule or Toxiproxy), then restore it.
Record how long it takes to get back to full connection count, and how many attempts failed on the way. With a fixed reconnect interval, every client retries at the same instant and the broker sees synchronized waves. The jittered reconnectPeriod in the script above spreads them out; compare both variants and you will see why reconnect storms are a client design problem as much as a broker capacity problem.
Connection stress test checklist
- Raise
ulimit -n, widenip_local_port_rangeand check conntrack on every load machine. - Plan source IPs: about 64,000 connections per source IP per broker endpoint.
- Match production: TLS, keep-alive, authentication and subscriptions per client.
- Ramp in steps and hold each one past several keep-alive intervals.
- Record broker connections, memory, CPU and connect latency at each step.
- Trigger a reconnect storm and measure recovery time.
- Repeat after broker upgrades or configuration changes.
A connection stress test is mostly about honesty: remove the limits on your own side, mirror real client behavior, and push until the broker — not the load generator — shows you where the ceiling is. Then plan your fleet comfortably below it, and make sure your clients reconnect with jitter so the ceiling is never tested by accident.
Frequently asked questions
Why does my MQTT connection test stop at about 28,000 clients?
That is typically the load generator running out of ephemeral ports: Linux uses roughly 28,000 local ports by default for connections from one source IP to one broker IP and port. Widen net.ipv4.ip_local_port_range or add more source IPs.
How many connections can an MQTT broker handle?
It depends on the broker, memory, CPU, TLS, keep-alive interval and message traffic, so there is no universal number. Measure it: ramp connections up while recording broker memory and CPU, and find the point where connect latency or errors rise.
Is it OK to stress test a public MQTT broker?
No. Opening thousands of connections to a shared public broker degrades it for everyone and will get you rate-limited or blocked. Run connection stress tests only against brokers you own or instances provided for that purpose.

