Skip to content

Publishing

The basic shapes

Typed, through the configured serializer:

csharp
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:

csharp
var outcome = await client.PublishAsync(new MqttPublishPacket
{
    Topic = "sensors/boiler-1/telemetry",
    Payload = payloadBytes,
    QualityOfService = MqttQualityOfService.AtLeastOnce,
    Retain = false,
}, token);

Quality of service

LevelWire exchangeThe returned task completes when
AtMostOnce (0)PUBLISHthe packet is flushed to the transport
AtLeastOnce (1)PUBLISH → PUBACKthe broker acknowledged
ExactlyOnce (2)PUBLISH → PUBREC → PUBREL → PUBCOMPthe 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:

DispositionMeaning
DeliveredThe broker received it (QoS > 0: acknowledged). ReasonCode carries the broker's answer.
QueuedThe client is offline; the message sits in the offline queue and flushes after reconnect, after re-subscription.
DroppedOfflineThe client is offline and QoS 0 messages are configured to drop (the default for QoS 0).
InFlightThe 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.
csharp
var outcome = await client.PublishAsync(packet, token);
if (outcome.Disposition == PublishDisposition.Queued)
{
    logger.LogWarning("Broker unreachable; message queued");
}

Retained messages

csharp
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:

csharp
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 TopicAliasMaximum on 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.

Released under the MIT License.