Load Testing

Stress tests of the broker on two benches — over the network and on loopback — to separate the broker's own behaviour from the environment. Below: method, full tables and short findings.

Download the report (PDF)

Benches

Date: 24 July 2026 · Protocol: MQTT 3.1.1 · Tool: in-house load client cmd/loadtest built on the project's own codec, no third-party MQTT library.

Testing was run on two benches to separate the broker's own behaviour from the influence of the environment: network vs. loopback, modest vs. powerful hardware.

Bench A — over the networkBench B — loopback
Broker CPU12 cores2× Xeon E5-2690 v4 @ 2.6 GHz (28 cores / 56 threads)
Broker RAM16 GB128 GB
Address192.168.1.84:1883 (LAN)127.0.0.1:1883 (loopback)
Load generatorSeparate machine on the LANSame machine as the broker
Client ephemeral ports16,384 (default)55,000 (widened)
How to read the comparison. The benches are not equivalent and answer different questions. Bench A shows operation over a network with a dedicated generator. Bench B removes the network (near-zero latency) and gives the broker powerful hardware, but the generator and broker share the same cores. So latency and fan-out are fair to compare head-to-head, while ingest rate is not: on Bench B the client steals cores from the broker.

Executive summary

  • Latency. Over the network, end-to-end “publisher → broker → subscriber” latency under normal load is about 1 ms; on loopback it is 73 µs (~16× lower, the network removed).
  • Connections. Over the network the broker held 10,000 sessions with no failures (beyond that we hit the client's port limit). On powerful hardware over loopback: 30,000 with no failures and ~41,000 at a 50,000 attempt (the limit of a single machine, not the broker).
  • Ingest. Peak over the network was ≈277,000 messages/s; on loopback ingest is lower (≈185,000/s) because the generator competes with the broker for cores.
  • Fan-out (routing). On powerful hardware the broker delivered ≈1,350,000 messages/s to subscribers (fan-out ×100) vs. ≈22,000/s over the network.
  • Robustness. On neither bench did the broker crash or hang. Under overload, QoS 0 messages to lagging subscribers are dropped gracefully; the broker log across every run contains not a single error.

Concurrent-connection capacity

Each connection is a full CONNECT → CONNACK cycle with authentication; the session is then held open (verified with PINGREQ).

Bench A — over the network

TargetEstablishedFailedSetup p50p99
10100146 ms162 ms
1001000702 ms1.12 s
1,0001,00004.21 s9.78 s
5,0005,00004.64 s8.59 s
10,00010,00004.92 s10.52 s
15,00014,8791217.43 s14.09 s

Bench B — loopback (gradual dial-up)

TargetEstablishedFailedSetup p50p99
1,0001,0000886 ms1.06 s
10,00010,0000903 ms1.55 s
30,00030,0000923 ms1.90 s
50,00041,2190984 ms1.90 s

Analysis

  • Over the network, up to 10,000 — zero failures; the 121 failures at 15,000 are exhaustion of the client's ephemeral ports (16,384). That is the generator's limit, not the broker's.
  • On loopback, after widening the port range, the broker held 30,000 sessions flawlessly; at 50,000 about 41,000 remained alive at the end of the hold — both sides share one machine. The broker log stayed clean throughout, no errors.
  • Side finding. On loopback an overly sharp burst of connections (1,000 simultaneous dials) triggers connection refused — the accept backlog overflows: with no network delay every SYN arrives at once. A gradual dial-up (200 at a time) removes the effect entirely. Over a network the latency smooths the burst on its own.

Ingest throughput

A pool of publishers publishes as fast as possible; the broker's ingest rate is measured.

QoS 0 · 64-byte payload

PublishersBench A (network)Bench B (loopback)
10271,006/s185,012/s
100236,422/s101,457/s
500103,546/s125,324/s

Ingest is higher over the network — not because the Bench A broker is faster, but because the generator runs on a separate machine there. On loopback the client and broker share the same cores, which caps the combined rate.

QoS 1 (with PUBACK acknowledgement) · 64 B

PublishersBench A (network)Bench B (loopback)
1010,292/s63,606/s
10022,085/s48,983/s

The QoS 1 test client is synchronous (it waits for a PUBACK after every message), so it is bound by the round-trip. On loopback the round-trip is near zero, hence the ~6× gain. This clearly shows the QoS 1 numbers are set by network latency, not by the broker.

Message-size sweep · QoS 0 · 50 publishers

SizeBench A (network)Bench B (loopback)
64 B277,198/s · 17.7 MB/s101,961/s · 6.5 MB/s
256 B124,997/s · 32.0 MB/s102,029/s · 26.1 MB/s
1,024 B38,458/s · 39.4 MB/s102,151/s · 104.6 MB/s

Two different ceilings:

  • Over the network we hit the link at ≈40 MB/s: as the message grows, msg/s falls while MB/s holds near the limit.
  • On loopback the link is enormous, so the message count holds steady (~102,000/s at 50 connections) and the volume rises to 104.6 MB/s. Here the bottleneck is per-message processing (CPU), not bytes.

End-to-end delivery

A timestamp is embedded in the message body; real delivery and end-to-end latency are measured. Fan-out means delivery to every subscriber matching the filter.

Bench A — over the network

ScenarioPublishDeliveryavg / p50 / p99
10 sub / 10 pub / 100 msg/s — normal986/s9,863/s1.17 / 1.0 / 3.6 ms
100 sub / 10 pub / unthrottled261,551/s22,351/s780 / 453 / 678 ms
100 sub / 10 pub / 500 msg/s4,934/s12,150/s1.87 / 1.77 / 2.83 s
1,000 sub / 5 pub / 100 msg/s494/s178,961/s2.29 s / 480 / 952 ms

Bench B — loopback

ScenarioPublishDeliveryavg / p50 / p99
10 sub / 10 pub / 100 msg/s — normal987/s9,867/s73 / ~0 / 555 µs
100 sub / 10 pub / unthrottled47,557/s1,353,431/s514 / 135 / 251 ms
100 sub / 10 pub / 500 msg/s3,608/s360,834/s17.1 / 10.5 / 71.6 ms
1,000 sub / 5 pub / 100 msg/s494/s183,746/s4.73 / 4.54 / 15.1 ms

Analysis

  • Latency under normal load: 1.17 ms over the network vs. 73 µs on loopback. The difference is the network path.
  • Routing under load: the powerful machine delivered ≈1.35 million messages/s to subscribers (fan-out ×100) — two orders of magnitude above the network bench (22k/s), where both the broker and the link are small.
  • In high-fan-out scenarios the generator also becomes the bottleneck (all subscribers in one process on one machine), so the peak delivery figures are a lower bound on the broker's capability.

Head-to-head

MetricBench A — network (12 cores)Bench B — loopback (28 cores)
Connections, no failures10,00030,000
Peak connections14,879 (client limit)~41,000 (single-machine limit)
Ingest QoS 0 (64 B), peak271,006/s185,012/s
Ingest QoS 1 (64 B), peak22,085/s63,606/s
Volume ceiling≈40 MB/s (link)≈105 MB/s (CPU)
Latency (normal), median1.0 ms~0 (73 µs avg)
Delivery at fan-out, peak22,351/s1,353,431/s
Errors in broker lognonenone

Methodology caveats

  • Ingest rate is not directly comparable between benches: on Bench B the generator and broker share cores; on Bench A the generator is a separate machine.
  • Load on each bench comes from a single client machine; in some scenarios (large fan-out, tens of thousands of connections) that machine, not the broker, is the bottleneck.
  • The QoS 1 test client is synchronous — QoS 1 figures understate the broker's potential.
  • Broker system metrics (CPU/RAM during runs) were not captured; server health is inferred from its log (no errors) and from client-side behaviour.

Reproduce

Every run is launched from one loadtest binary; below are the commands for the three modes.

go build -o loadtest ./cmd/loadtest

# Connection capacity (gradual dial-up vs. accept backlog)
./loadtest -addr HOST:1883 -user myelx -pass *** \
    -mode conn -levels 1000,10000,30000,50000 -hold 3s -dial-concurrency 200

# Ingest throughput
./loadtest -addr HOST:1883 -user myelx -pass *** \
    -mode pub -pubs 10 -qos 0 -payload 64 -duration 10s

# End-to-end delivery and latency
./loadtest -addr HOST:1883 -user myelx -pass *** \
    -mode pubsub -subs 10 -pubs 10 -qos 0 -rate 100 -payload 64 -duration 10s