Performance
Performance claims here are measured, repeatable, and published — including the scenarios where Pulse does not come out ahead. Hardware for all numbers below: Intel i7-12700H, Windows 11, .NET 10, workstation GC.
How it compares
For a familiar reference point, the numbers below put Pulse.Mqtt next to MQTTnet 5.1 — both libraries driving their user-facing clients against the same real Mosquitto broker, with identical payloads, topics, and QoS, and the broker's inflight cap removed so the clients are measured rather than the cap. Full methodology, tables, and the honest caveats: the benchmark comparison.
| Metric | Pulse.Mqtt | MQTTnet 5.1 |
|---|---|---|
| Allocated per message, sustained QoS 1 load | 1,440 B | 1,645 / 1,728 B |
| GC under that load (gen0/1/2) | 2 / 0 / 0 | 2 / 2 / 0 |
| Allocation per operation (QoS 0/1/2) | 31–67% less in every scenario | — |
| Sustained throughput (20k QoS 1, 200 in flight) | 3,734 msg/s | 3,869 / 4,091 msg/s |
| Receive-maximum compliance | enforced | not enforced |
| Per-operation latency | parity within noise; leads trade places between runs | — |
The allocation and GC rows are stable across every environment this has run on; wall-clock rows move with the Docker networking stack (the page notes the re-baseline). Against an out-of-the-box Mosquitto, Pulse correctly holds the advertised 20-message in-flight window while MQTTnet exceeds it — a protocol violation a broker may answer with a disconnect.
Micro-benchmarks
From the benchmark suite, which mirrors the upstream MQTTnet benchmark project scenario for scenario:
| Operation | Mean | Allocated |
|---|---|---|
| PUBLISH encode (v5, no properties) | ~60 ns | 0 B |
| Frame + decode the same packet | ~96 ns | 144 B (the decoded object) |
| Topic filter match | ~32 ns | 0 B |
| Route template match (2 captures) | ~56 ns | 104 B (the captured values) |
| Variable-length integer round trip | ~26 ns | 0 B |
| 10,000 awaited publishes through the in-process broker | 865 ns each | 350 B (both sides) |
| Subscribe 10,000 topics, each acknowledged | 119 ms | 19 MB |
Where the numbers come from
- Single-pass encoding. A publish without v5 properties is sized up front and written straight into the transport buffer — zero intermediate allocation, the payload copied once.
- Single-write framing. One packet, one TCP write: no fragmentation, no Nagle stalls on proxied paths (a default-configuration MQTTnet publish lost ~40 ms per operation to exactly that in our measurements).
- Receive-loop dispatch. Acknowledgements complete their waiters directly on the receive loop; inbound messages take one bounded queue to your handler, not three.
- Bounded queues everywhere. Backpressure flows to the socket; the GC table above — zero gen 1 collections under sustained load — is the visible result.
- Pooled buffers for everything that cannot be encoded in place.
Reproduce everything
# Micro-benchmarks (no infrastructure):
dotnet run -c Release --project bench/Pulse.Mqtt.Benchmarks -- --filter * --job short
# Comparison against MQTTnet (requires Docker for Mosquitto):
dotnet run -c Release --project bench/Pulse.Mqtt.ComparisonBenchmarks