Publishing
The basic shapes
Typed, through the configured serializer:
var outcome = await client.PublishAsync(
"sensors/boiler-1/telemetry",
new TelemetryReading("C", 21.5, DateTimeOffset.UtcNow),
MqttQualityOfService.AtLeastOnce,
retain: false,
cancellationToken: token);Raw, with full control over the packet:
var outcome = await client.PublishAsync(new MqttPublishPacket
{
Topic = "sensors/boiler-1/telemetry",
Payload = payloadBytes,
QualityOfService = MqttQualityOfService.AtLeastOnce,
Retain = false,
}, token);Quality of service
| Level | Wire exchange | The returned task completes when |
|---|---|---|
AtMostOnce (0) | PUBLISH | the packet is flushed to the transport |
AtLeastOnce (1) | PUBLISH → PUBACK | the broker acknowledged |
ExactlyOnce (2) | PUBLISH → PUBREC → PUBREL → PUBCOMP | the full exchange completed |
Packet identifiers are assigned by the client — never set PacketIdentifier yourself. QoS 1 and 2 publishes are awaited to their acknowledgement with AcknowledgementTimeout as the upper bound.
Outcomes — no silent loss
Every publish returns a PublishOutcome:
| Disposition | Meaning |
|---|---|
Delivered | The broker received it (QoS > 0: acknowledged). ReasonCode carries the broker's answer. |
Queued | The client is offline; the message sits in the offline queue and flushes after reconnect, after re-subscription. |
DroppedOffline | The client is offline and QoS 0 messages are configured to drop (the default for QoS 0). |
InFlight | The connection dropped mid-exchange while a persistent session was tracking this QoS 1/2 publish. The message is held and redelivers (with DUP) on resume. |
var outcome = await client.PublishAsync(packet, token);
if (outcome.Disposition == PublishDisposition.Queued)
{
logger.LogWarning("Broker unreachable; message queued");
}Retained messages
new MqttPublishPacket { Topic = "devices/boiler-1/config", Payload = bytes, Retain = true }The broker stores the last retained message per topic and hands it to new subscribers immediately.
MQTT 5 properties
The publish packet exposes the full v5 property set:
new MqttPublishPacket
{
Topic = "orders/created",
Payload = bytes,
ContentType = "application/json",
PayloadFormatIndicator = MqttPayloadFormatIndicator.Utf8,
MessageExpiryInterval = 3600, // seconds
ResponseTopic = "orders/created/ack", // see request/response
CorrelationData = correlationBytes,
UserProperties = [new MqttUserProperty("tenant", "acme")],
}Typed publishes stamp ContentType and PayloadFormatIndicator from the serializer automatically.
These properties are MQTT 5-only. With MqttProtocolVersion.V311, the publish can still carry the topic, payload, QoS, retain flag, and packet identifier, but MQTT 5 metadata has no wire slot. Pulse rejects MQTT 5-only publish properties on MQTT 3.1.1 packets instead of silently dropping them. If an MQTT 3.1.1 deployment needs expiry, content type, correlation, or custom metadata, encode it in the payload or topic convention. See the MQTT protocol compatibility matrix.
Publishing while offline
Nothing special to write — publish as usual and read the outcome. QoS 1/2 messages are queued (bounded, with your chosen overflow policy) and flushed in order after the next successful reconnect; QoS 0 messages drop by default or queue with IncludeQos0 = true. Details and tuning live in Resilience.
Size limits
When the broker advertises a maximum packet size in its CONNACK, the client enforces it: a publish whose encoded size (fixed header, properties, payload) exceeds the limit fails immediately with MqttPacketTooLargeException — before any byte reaches the wire, so the connection never gets killed with PacketTooLarge. The exception carries the actual size and the limit. A too-large publish is never silently queued; and if a queued message turns out too large for a stricter broker after a reconnect, it is dropped loudly (logged, counted as DroppedTooLarge) so the rest of the queue keeps flushing. Brokers that advertise no limit cost the send path nothing.
Concurrency
PublishAsync is safe to call from any number of tasks concurrently. Sends are serialized on the connection in call order; QoS 1/2 acknowledgements complete out of order as the broker answers, so a slow acknowledgement never blocks the pipe.
The in-flight window honors the broker's CONNACK receive maximum: QoS 1/2 publishes beyond the limit wait (cancellable) until an acknowledgement frees a slot — PUBACK for QoS 1, PUBREC for QoS 2, exactly as the specification counts the quota. The limit re-arms on every reconnect from the new CONNACK; brokers that advertise none leave the window bounded only by packet-identifier availability (65,535).
Topic aliases
MQTT 5 topic aliases replace a repeated topic string with a two-byte alias. Both directions are covered:
- Inbound is automatic: advertise a capacity by setting
TopicAliasMaximumon the CONNECT packet, and publishes the broker compresses are resolved back to their full topic before your handlers see them. Violations (an unestablished or out-of-range alias) are protocol errors that fault the session, per the specification. - Outbound is opt-in — set
RawMqttClientOptions.UseOutboundTopicAliases = true(ResilientMqttClientOptions.Raw). Within the maximum the broker advertises, the first publish to a topic establishes an alias and every repeat sends two bytes instead of the topic string. Assignment is first come, first served; topics beyond the broker's maximum simply go plain. Aliases reset on every reconnect, automatically.
On a 60-character topic the alias form is both smaller on the wire and faster to encode — see the benchmark suite's topic-alias comparison.
Performance notes
- A publish without v5 properties encodes in a single pass with zero allocation — the payload is copied exactly once, into the transport buffer.
- Each packet is one TCP write: no fragmentation, no Nagle interaction on proxied paths.
- See Performance for measured numbers.