MQTT QoS 1 guarantees that a message will be delivered to the recipient at least once, but it might arrive more than once.

Let’s watch this in action. Imagine we have a simple MQTT broker running locally on port 1883, and two clients: a publisher sending messages and a subscriber receiving them.

Publisher (publisher.py):

import paho.mqtt.client as mqtt
import time

broker_address = "localhost"
port = 1883
topic = "sensor/temperature"

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("Connected to MQTT Broker!")
    else:
        print(f"Failed to connect, return code {rc}\n")

client = mqtt.Client()
client.on_connect = on_connect
client.connect(broker_address, port)
client.loop_start()

message_count = 0
while message_count < 5:
    message = f"temp:{20 + message_count}"
    result = client.publish(topic, message, qos=1)
    # result[0] is the MID returned by the broker to the client
    status = result[0]
    if status == 0:
        print(f"Sent `{message}` to topic `{topic}` (MID: {result[1]})")
    else:
        print(f"Failed to send message to topic {topic}")
    message_count += 1
    time.sleep(2)

client.loop_stop()
client.disconnect()
print("Publisher finished.")

Subscriber (subscriber.py):

import paho.mqtt.client as mqtt
import time

broker_address = "localhost"
port = 1883
topic = "sensor/temperature"

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("Connected to MQTT Broker!")
        client.subscribe(topic, qos=1)
        print(f"Subscribed to topic: {topic} with QoS 1")
    else:
        print(f"Failed to connect, return code {rc}\n")

def on_message(client, userdata, msg):
    print(f"Received message: '{msg.payload.decode()}' on topic '{msg.topic}' with QoS {msg.qos}")
    # Simulate some processing time
    time.sleep(1)

client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.connect(broker_address, port)
client.loop_forever()

When you run the publisher and then the subscriber, you’ll see output like this:

Publisher Output:

Connected to MQTT Broker!
Sent `temp:20` to topic `sensor/temperature` (MID: 1)
Sent `temp:21` to topic `sensor/temperature` (MID: 2)
Sent `temp:22` to topic `sensor/temperature` (MID: 3)
Sent `temp:23` to topic `sensor/temperature` (MID: 4)
Sent `temp:24` to topic `sensor/temperature` (MID: 5)
Publisher finished.

Subscriber Output:

Connected to MQTT Broker!
Subscribed to topic: sensor/temperature with QoS 1
Received message: 'temp:20' on topic 'sensor/temperature' with QoS 1
Received message: 'temp:21' on topic 'sensor/temperature' with QoS 1
Received message: 'temp:22' on topic 'sensor/temperature' with QoS 1
Received message: 'temp:23' on topic 'sensor/temperature' with QoS 1
Received message: 'temp:24' on topic 'sensor/temperature' with QoS 1

The core problem MQTT QoS 1 solves is network unreliability. In many real-world scenarios, messages can be lost due to transient network issues, overloaded brokers, or slow clients. If you’re sending critical data, like sensor readings or control commands, you can’t afford to lose messages. QoS 0 (at most once) is too risky, and QoS 2 (exactly once) can be overkill in terms of performance overhead. QoS 1 strikes a balance: it ensures delivery without demanding the strict two-phase handshake of QoS 2.

Here’s how it works under the hood:

When a publisher sends a message with QoS 1, it assigns a unique Message ID (MID) to that message. The publisher then waits for a PUBACK (Publish Acknowledge) packet from the broker. This PUBACK contains the same MID as the original published message. If the publisher doesn’t receive this PUBACK within a certain time, it will retransmit the message with the same MID. This retransmission continues until the PUBACK is received. This is the "at least once" guarantee: if the message is lost, it’s resent. If the PUBACK is lost, the message might be resent even if the broker already received it.

On the broker’s side, when it receives a QoS 1 message, it sends a PUBACK back to the publisher. The broker also ensures that this message is delivered to all subscribed clients. If a subscriber is offline when the message arrives, the broker will store it until the subscriber reconnects. When the subscriber reconnects, the broker will attempt to deliver the stored message.

The subscriber, upon receiving a QoS 1 message, must send a PUBACK back to the broker. This PUBACK signals to the broker that the message has been successfully received by the subscriber. The broker can then safely discard its copy of the message. If the subscriber crashes after receiving the message but before sending the PUBACK, the broker will re-deliver the message when the subscriber comes back online. This is why duplicates can occur: the subscriber might have processed the message, but the broker never got the acknowledgement, so it re-sends it.

The result object returned by client.publish() in paho-mqtt is crucial. result[0] tells you the status of the publish operation from the client’s perspective. A status of 0 indicates that the client successfully sent the PUBLISH packet to the network. result[1] is the MID. The client library handles the waiting for PUBACK and retransmissions automatically if loop_start() or loop_forever() is used.

One thing that often trips people up is how the broker handles message delivery to subscribers. The broker doesn’t just blindly forward every message. When it receives a QoS 1 message, it looks up all active subscribers for that topic. For each subscriber, it sends the PUBLISH packet. Only after it has sent the PUBLISH packet to a specific subscriber does it then wait for that subscriber to send back a PUBACK for that specific message. If the broker doesn’t receive a PUBACK from a subscriber for a message it sent, it will keep retrying delivery to that subscriber. This is how the broker ensures the message reaches the subscriber, and it’s also the mechanism that leads to potential duplicates if the subscriber’s PUBACK is lost.

If you want to manage the PUBACK process more granularly on the subscriber side, you can use client.publish_release(mid) after you’ve processed the message. This tells the broker that you’ve handled the message and it can stop retransmitting it to you.

The next logical step after understanding at-least-once delivery is exploring the nuances of handling duplicate messages on the subscriber side, or moving on to the more robust, but more complex, exactly-once delivery offered by QoS 2.

Want structured learning?

Take the full Mqtt course →