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 network | Bench B — loopback | |
|---|---|---|
| Broker CPU | 12 cores | 2× Xeon E5-2690 v4 @ 2.6 GHz (28 cores / 56 threads) |
| Broker RAM | 16 GB | 128 GB |
| Address | 192.168.1.84:1883 (LAN) | 127.0.0.1:1883 (loopback) |
| Load generator | Separate machine on the LAN | Same machine as the broker |
| Client ephemeral ports | 16,384 (default) | 55,000 (widened) |
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
| Target | Established | Failed | Setup p50 | p99 |
|---|---|---|---|---|
| 10 | 10 | 0 | 146 ms | 162 ms |
| 100 | 100 | 0 | 702 ms | 1.12 s |
| 1,000 | 1,000 | 0 | 4.21 s | 9.78 s |
| 5,000 | 5,000 | 0 | 4.64 s | 8.59 s |
| 10,000 | 10,000 | 0 | 4.92 s | 10.52 s |
| 15,000 | 14,879 | 121 | 7.43 s | 14.09 s |
Bench B — loopback (gradual dial-up)
| Target | Established | Failed | Setup p50 | p99 |
|---|---|---|---|---|
| 1,000 | 1,000 | 0 | 886 ms | 1.06 s |
| 10,000 | 10,000 | 0 | 903 ms | 1.55 s |
| 30,000 | 30,000 | 0 | 923 ms | 1.90 s |
| 50,000 | 41,219 | 0 | 984 ms | 1.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) triggersconnection 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
| Publishers | Bench A (network) | Bench B (loopback) |
|---|---|---|
| 10 | 271,006/s | 185,012/s |
| 100 | 236,422/s | 101,457/s |
| 500 | 103,546/s | 125,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
| Publishers | Bench A (network) | Bench B (loopback) |
|---|---|---|
| 10 | 10,292/s | 63,606/s |
| 100 | 22,085/s | 48,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
| Size | Bench A (network) | Bench B (loopback) |
|---|---|---|
| 64 B | 277,198/s · 17.7 MB/s | 101,961/s · 6.5 MB/s |
| 256 B | 124,997/s · 32.0 MB/s | 102,029/s · 26.1 MB/s |
| 1,024 B | 38,458/s · 39.4 MB/s | 102,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
| Scenario | Publish | Delivery | avg / p50 / p99 |
|---|---|---|---|
| 10 sub / 10 pub / 100 msg/s — normal | 986/s | 9,863/s | 1.17 / 1.0 / 3.6 ms |
| 100 sub / 10 pub / unthrottled | 261,551/s | 22,351/s | 780 / 453 / 678 ms |
| 100 sub / 10 pub / 500 msg/s | 4,934/s | 12,150/s | 1.87 / 1.77 / 2.83 s |
| 1,000 sub / 5 pub / 100 msg/s | 494/s | 178,961/s | 2.29 s / 480 / 952 ms |
Bench B — loopback
| Scenario | Publish | Delivery | avg / p50 / p99 |
|---|---|---|---|
| 10 sub / 10 pub / 100 msg/s — normal | 987/s | 9,867/s | 73 / ~0 / 555 µs |
| 100 sub / 10 pub / unthrottled | 47,557/s | 1,353,431/s | 514 / 135 / 251 ms |
| 100 sub / 10 pub / 500 msg/s | 3,608/s | 360,834/s | 17.1 / 10.5 / 71.6 ms |
| 1,000 sub / 5 pub / 100 msg/s | 494/s | 183,746/s | 4.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
| Metric | Bench A — network (12 cores) | Bench B — loopback (28 cores) |
|---|---|---|
| Connections, no failures | 10,000 | 30,000 |
| Peak connections | 14,879 (client limit) | ~41,000 (single-machine limit) |
| Ingest QoS 0 (64 B), peak | 271,006/s | 185,012/s |
| Ingest QoS 1 (64 B), peak | 22,085/s | 63,606/s |
| Volume ceiling | ≈40 MB/s (link) | ≈105 MB/s (CPU) |
| Latency (normal), median | 1.0 ms | ~0 (73 µs avg) |
| Delivery at fan-out, peak | 22,351/s | 1,353,431/s |
| Errors in broker log | none | none |
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