Skip to content

Manual Acknowledgement

By default, zmqtt advances the incoming QoS handshake automatically before it exposes the delivery to application code. With auto_ack=False you take control: PUBACK or PUBREC is withheld until you explicitly call msg.ack().

Why use manual ack

Use auto_ack=False when acknowledgement must happen after processing. The broker does not receive the acknowledgement until your handler calls ack().

Manual acknowledgement controls this ordering, but it does not make the handler transactional. Redelivery after a lost connection or process restart also depends on the broker session configuration.

Note

Manual ack only has effect at QoS 1 (AT_LEAST_ONCE) or QoS 2 (EXACTLY_ONCE). At QoS 0 (AT_MOST_ONCE) there is no acknowledgement protocol, so auto_ack=False is a no-op. See QoS levels for the delivery guarantees.

Enabling manual ack

async with client.subscribe("jobs/#", qos=QoS.AT_LEAST_ONCE, auto_ack=False) as sub:
    async for msg in sub:
        await process(msg)   # do your work first
        await msg.ack()      # then acknowledge

msg.ack() is idempotent — calling it multiple times is safe, subsequent calls are no-ops.

QoS 1 semantics

For QoS 1 messages (AT_LEAST_ONCE), ack() sends PUBACK to the broker. Until PUBACK is sent, the broker may retransmit the message. Each retransmission appears as a new Message in your queue, so handlers should be idempotent.

QoS 2 semantics

For QoS 2 messages (EXACTLY_ONCE), ack() sends PUBREC. The library then handles PUBREL and sends PUBCOMP automatically.

QoS 2 deduplication during the manual-ack window

Between receiving the initial PUBLISH and calling ack(), PUBREC has not been sent, so the broker may retransmit the PUBLISH. Retransmitted PUBLISH packets for that delivery are ignored while the same connection remains active and the message is still unacknowledged.

Connection loss before ack()

Manual acknowledgement alone does not preserve an unacknowledged delivery across reconnects. Broker redelivery requires a stable client_id and a persistent session:

MQTT 3.1.1:

client = create_client(
    "localhost",
    client_id="jobs-worker-1",
    clean_session=False,
)

MQTT 5.0:

client = create_client(
    "localhost",
    version="5.0",
    client_id="jobs-worker-1",
    clean_session=False,
    session_expiry_interval=3600,
)

With a persistent broker session, an unacknowledged QoS 1 or QoS 2 message may be delivered again after reconnect. QoS 2 duplicate suppression does not extend across reconnects or process restarts, so do not rely on it for exactly-once execution of application code.

The default connection settings start a clean session, so they do not provide durable redelivery after a connection or process is lost.

Example: persistent job consumer

import asyncio
from zmqtt import create_client, QoS

async def main():
    async with create_client(
        "localhost",
        client_id="jobs-worker-1",
        clean_session=False,
    ) as client:
        async with client.subscribe(
            "jobs/process",
            qos=QoS.AT_LEAST_ONCE,
            auto_ack=False,
        ) as sub:
            async for msg in sub:
                try:
                    await process_job(msg.payload)
                    await msg.ack()
                except Exception:
                    # Leave the delivery unacknowledged. A persistent broker
                    # session makes it eligible for redelivery.
                    raise

asyncio.run(main())

See also: Publishing — QoS levels · Reconnection · Error Handling