A high-performance, low-allocation MQTT 3.1.1 + 5.0 client for .NET — designed to feel like System.Threading.Channels.
- Multi-TFM:
netstandard2.0,netstandard2.1,net8.0,net9.0,net10.0(NativeAOT-clean on net10.0; WebSocket transport requires netstandard2.1+). - Channels-style API —
ChannelReader<MqttMessage>for subscriptions,TryPublish/PublishAsyncfor sending. - Built-in DI extensions, source-generated logging,
System.Diagnostics.Metrics,ActivitySource. - Transports: TCP, TLS, WebSocket, Secure WebSocket.
- SOCKS5 proxy (RFC 1928, with optional RFC 1929 username/password auth) for TCP/TLS.
- Auto-reconnect with exponential backoff + jitter; queued publishes survive reconnect.
- Pluggable session persistence for QoS 1/2 in-flight state.
- Secure defaults — TLS 1.2/1.3, CRL checking, capped incoming packet size.
dotnet add package Mqtt.Clientusing Mqtt.Client;
await using var client = MqttClient.CreateBuilder()
.ConnectTo("mqtts://broker:8883")
.WithClientId("svc-1")
.WithCredentials("user", "pw")
.Build();
await client.ConnectAsync();
await using var sub = await client.SubscribeAsync("sensors/+/temp");
await foreach (var msg in sub.Reader.ReadAllAsync())
{
using (msg) // payloads are pooled by default — dispose after use, don't retain
{
Console.WriteLine($"{msg.Topic}: {msg.Payload.Length} bytes");
}
}
await client.PublishAsync("commands/svc-1", "ping"u8.ToArray());// Publish a payload that's already split across buffers — no concatenation needed.
ReadOnlySequence<byte> framed = BuildFramedPayload();
await client.PublishAsync("telemetry", framed, MqttQoS.AtLeastOnce);
// Inline-handler subscription: the payload is a true zero-copy slice of the receive buffer,
// valid only inside the handler (no allocation, nothing to dispose). Back-pressure flows to
// the broker while the handler runs.
await client.SubscribeAsync("telemetry/#", msg =>
{
Process(msg.PayloadMemory.Span); // do not retain msg or its payload
return ValueTask.CompletedTask;
});
// Channel subscriptions pool the payload by default (dispose each message). If you need to
// retain messages freely instead, opt back into garbage-collected payloads:
var retaining = MqttClient.CreateBuilder()
.ConnectTo("mqtt://broker:1883")
.Configure(o => o.RetainableInboundMessages = true)
.Build();- Quickstart
- Core concepts — channels-style, threading, backpressure
- Samples — auth, TLS, DI, last-will, MQTT 5 properties
- Advanced — custom transport, persistence, AOT, metrics
- Troubleshooting
- Spec conformance
- Chaos / soak testing — continuous-recovery, no-leak, no-hang ruggedization gate
- Benchmarks — Mqtt.Client vs MQTTnet (full matrix) plus cross-language throughput vs C (Mosquitto, Paho)
- Mqtt.Client.Testing — companion package: an embeddable, in-process MQTT broker for tests (no install, all TFMs, parallel-isolated)
🔍 When to pick MQTTnet instead
MQTTnet is a mature, battle-tested .NET MQTT library that covers ground this client
intentionally doesn't:
- You need a broker in addition to a client.
- You need ASP.NET Core integration (managed clients, hosted broker, middleware).
- You need the broadest possible protocol option coverage and a long support history.
- You're already on it and it serves you well.
Mqtt.Client is focused on being a small, fast, AOT-friendly, channels-style
client library for .NET services. Pick whichever fits your situation; both projects
are MIT-licensed and the protocol wire formats are identical.
Found something? Please file privately via GitHub Security Advisories — see SECURITY.md.
The published Mqtt.Client and Mqtt.Client.Testing assemblies are strong-named, so they can be referenced from other strong-named projects. The public key token is 7101d3319b65c487.
Strong naming is an assembly identity feature, not a security boundary: it does not authenticate the publisher, and the packages are not Authenticode- or NuGet-signed. The signing key (Mqtt.Client.snk) is committed to this repository and is intentionally not a secret. The same key is reused for every release so the strong-name identity stays stable across versions.