Manual Acknowledgement of Incoming Publishes
The ability to manually acknowledge incoming publishes was added in v0.40.0.
When manual ack is enabled, the client does not send PubAck (QoS 1) or PubRec (QoS 2) to the broker until your application explicitly acknowledges each received message. This gives you control over when the broker considers a message delivered—for example, after persisting it or completing business logic.
Use manual ack when you need to process or store messages before the broker is told they were received. Unacknowledged messages consume slots in the Receive Maximum window until you call AckAsync or the connection closes.
Enabling manual ack
Enable manual acknowledgement when building client options:
using HiveMQtt.Client;
var options = new HiveMQClientOptionsBuilder()
.WithBroker("broker.example.com")
.WithPort(1883)
.WithManualAck() // or .WithManualAck(true)
.Build();
var client = new HiveMQClient(options);
await client.ConnectAsync();
To disable (default): .WithManualAck(false) or omit the call.
Acknowledging messages
Two overloads are available on both IHiveMQClient and IRawClient:
By packet identifier
For QoS 1 and QoS 2, each received publish has a packet identifier. Use it to acknowledge:
client.OnMessageReceived += (sender, args) =>
{
// Only QoS 1 and 2 have a packet identifier
if (args.PacketIdentifier.HasValue)
{
_ = client.AckAsync(args.PacketIdentifier.Value);
}
};
By event args (recommended when mixing QoS levels)
When your subscription receives both QoS 0 and QoS 1/2, use the event-args overload. It no-ops for QoS 0 (no packet identifier) and acknowledges for QoS 1 and 2:
client.OnMessageReceived += async (sender, args) =>
{
// Process the message (e.g. persist, forward)...
await ProcessMessageAsync(args.PublishMessage);
// Safe for any QoS: no-op for QoS 0, sends PubAck/PubRec for QoS 1/2
await client.AckAsync(args);
};
This avoids having to check PacketIdentifier and prevents accidentally using it when it is null (QoS 0).
QoS behavior
| QoS | Packet identifier | Manual ack behavior |
|---|---|---|
| 0 | null | No ack is sent. AckAsync(args) is a no-op. |
| 1 | Set | Call AckAsync to send PubAck to the broker. |
| 2 | Set | Call AckAsync to send PubRec; client completes QoS 2 flow when broker sends PubRel. |
For QoS 0, OnMessageReceivedEventArgs.PacketIdentifier is always null. The client does not send any acknowledgement to the broker for QoS 0.
Receive Maximum and unacked messages
The broker limits how many QoS 1 and QoS 2 publishes can be “in flight” to the client (Receive Maximum). With manual ack enabled, each message you have not yet acknowledged consumes one of those slots. If you receive more messages than the window size without acknowledging, the broker will stop delivering until you call AckAsync (or disconnect). Configure a larger window if you need more in-flight messages:
var options = new HiveMQClientOptionsBuilder()
.WithBroker("broker.example.com")
.WithManualAck()
.WithReceiveMaximum(100) // Allow more unacked messages
.Build();
Manual ack, ordering, and Receive Maximum
QoS 1 and QoS 2 OnMessageReceived handlers are started in FIFO order on a per-client dispatch queue. See Message Ordering for the full guarantee (including async handler limits).
With manual ack enabled:
- Each unacknowledged message holds a Receive Maximum slot until you call
AckAsync. - Ordered dispatch means a slow handler delays the start of later handlers; slots remain held until each handler acknowledges.
- If handlers are slow and the dispatch queue backs up, the broker may stop sending new QoS 1/2 messages sooner — that is expected back-pressure, not a protocol error.
Keep handlers thin (parse and enqueue), perform heavy work on application-owned consumers, and call AckAsync when your processing is complete (or when you are ready for the broker to advance the window).
Exceptions
- Manual ack not enabled: Calling
AckAsyncwhenManualAckEnabledisfalsethrowsHiveMQttClientException. - Invalid packet identifier: If no pending incoming publish exists for the given packet id (e.g. wrong id or already acked),
AckAsyncthrowsHiveMQttClientException. - Double ack: Acknowledging the same packet identifier more than once throws
HiveMQttClientException(e.g. “Packet identifier X was already acknowledged.”). - Not connected: Calling
AckAsyncwhen the client is not connected throwsHiveMQttClientException. - Null event args:
AckAsync(OnMessageReceivedEventArgs eventArgs)throwsArgumentNullExceptionifeventArgsis null.
Thread safety
QoS 1 and QoS 2 OnMessageReceived handlers run on a dedicated message dispatch thread. QoS 0 handlers may run on thread-pool threads.
AckAsync may be called directly from your OnMessageReceived handler, including from the dispatch thread. You do not need to marshal the call back to a specific thread.
Full example (HiveMQClient)
using HiveMQtt.Client;
using HiveMQtt.MQTT5.Types;
var options = new HiveMQClientOptionsBuilder()
.WithBroker("broker.example.com")
.WithPort(1883)
.WithManualAck()
.Build();
var client = new HiveMQClient(options);
client.OnMessageReceived += async (sender, args) =>
{
try
{
// Your processing (e.g. save to DB, forward)
await SaveToDatabaseAsync(args.PublishMessage);
}
finally
{
// Always ack when manual ack is enabled (no-op for QoS 0)
await client.AckAsync(args);
}
};
await client.ConnectAsync();
await client.SubscribeAsync("orders/#", QualityOfService.AtLeastOnceDelivery);
RawClient
Manual ack works the same with RawClient: enable with WithManualAck() and use AckAsync(packetIdentifier) or AckAsync(eventArgs) on the RawClient instance. Use the packet identifier from your receive path (e.g. from the publish packet or from higher-level event args if you build them).
See also
- HiveMQClientOptions Reference —
ManualAckEnabled - HiveMQClientOptionsBuilder Reference —
WithManualAck - Lifecycle Events —
OnMessageReceivedand event args - MQTT 5 Essentials – Receive Maximum