Skip to content

Broker compatibility

Pulse.Mqtt is verified against three brokers on every change through a shared conformance suite. Each broker runs the same scenarios — defined once in BrokerScenarios — so a failure names the broker and the capability that regressed rather than a one-off test.

Tested brokers

BrokerVersion testedAnonymous accessHow it runs
Eclipse Mosquitto2.x (eclipse-mosquitto:2)mosquitto-no-auth.confEvery PR and every merge
EMQX5.8 (emqx/emqx:5.8)Default listenerPRs that touch source, merges to main, on demand
EMQX over QUIC5.8, QUIC listener on 14567/udpDefault listenerSame as EMQX; needs msquic on the machine
HiveMQ CE2024.3 (hivemq/hivemq-ce:2024.3)Default listenerPRs that touch source, merges to main, on demand

The brokers run as Testcontainers images, so the suite needs only a working Docker daemon — no broker installs, no shared test environment.

Verified scenarios

Each broker passes all of the following:

ScenarioWhat it proves
HandshakeCONNECT/CONNACK succeeds and a clean DISCONNECT is accepted.
QoS 0 round tripFire-and-forget delivery.
QoS 1 round tripAt-least-once delivery with PUBACK.
QoS 2 round tripExactly-once delivery through the full PUBREC/PUBREL/PUBCOMP exchange.
Retained messageA retained publish reaches a subscriber that connects after it.
Shared subscription$share/<group>/<topic> delivers each message to exactly one group member.
Large payloadA 64 KB payload round-trips intact.
Persistent session resumeReconnecting with CleanStart = false and a non-zero session expiry resumes the session (SessionPresent = true) and still routes its earlier subscription.
Receive-maximum flow controlA QoS 2 burst larger than the broker's receive maximum completes and arrives exactly once — the client holds a send-quota slot per exchange until PUBCOMP, so it never exceeds the broker's limit.
Topic aliasesA repeated topic is compressed into an MQTT 5 topic alias after the first publish; the broker accepts the alias and still routes every message to the full topic.

TLS is covered separately by a dedicated integration test (TlsIntegrationTests) that round-trips a message over a real TLS connection to a Mosquitto broker configured with a generated self-signed certificate.

The EMQX-over-QUIC row runs the identical scenario suite through the QUIC transport against the same EMQX container's QUIC listener, so transport differences cannot hide behind broker differences.

How the matrix is gated in CI

Running the heavier EMQX and HiveMQ images on every push would slow the inner loop, so the matrix is split:

  • ci.yml runs on every PR and push with --filter "Category!=BrokerMatrix". Mosquitto and all other tests run; the EMQX/HiveMQ classes are skipped, keeping the fast lane quick.
  • broker-matrix.yml runs --filter "Category=BrokerMatrix" (EMQX and HiveMQ) on PRs that touch the source, the integration tests, or the shared build inputs — so a cross-broker regression is caught before merge — and again on main and via manual dispatch. A matrix failure on main opens (or updates) a tracking issue, since it can't block the merge that caused it.

The EMQX and HiveMQ test classes carry [Trait("Category", "BrokerMatrix")]; Mosquitto does not, so it always runs.

Running the matrix locally

bash
# Mosquitto only (the fast inner loop)
dotnet test tests/Pulse.Mqtt.IntegrationTests --filter "Category!=BrokerMatrix"

# EMQX and HiveMQ (pulls the broker images on first run)
dotnet test tests/Pulse.Mqtt.IntegrationTests --filter "Category=BrokerMatrix"

Released under the MIT License.