Skip to main content

Manual Acknowledgement of Incoming Publishes

Version Note

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.

When to use manual ack

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);
}
};

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

QoSPacket identifierManual ack behavior
0nullNo ack is sent. AckAsync(args) is a no-op.
1SetCall AckAsync to send PubAck to the broker.
2SetCall 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 AckAsync when ManualAckEnabled is false throws HiveMQttClientException.
  • Invalid packet identifier: If no pending incoming publish exists for the given packet id (e.g. wrong id or already acked), AckAsync throws HiveMQttClientException.
  • Double ack: Acknowledging the same packet identifier more than once throws HiveMQttClientException (e.g. “Packet identifier X was already acknowledged.”).
  • Not connected: Calling AckAsync when the client is not connected throws HiveMQttClientException.
  • Null event args: AckAsync(OnMessageReceivedEventArgs eventArgs) throws ArgumentNullException if eventArgs is 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