Delivery and Retry Semantics
ReferenceAudience: API consumerUse: design retries, duplicate handling, and loss handling for commands, events, and tag data
MQTT delivery guarantees apply between each client and the broker, not end to end. This page states what the contract declares for each message, and what that means for retries, duplicates, and loss.
Declared QoS and RETAIN
The contract declares these values on its operations. On the dedicated data channels, it leaves QoS to the topic's own configuration:
| Message | Direction | Contract channel | QoS | RETAIN |
|---|---|---|---|---|
| Commands (all 20) | Application → reader | command | 1 | false |
| Command responses | Reader → application | response | 1 | false |
alerts | Reader → application | event | 1 | false |
heartBeatEVT | Reader → application | event | 0 | false |
mqttConnEVT | Reader → application | rfidData | 0 | Not settled: the operation declares true, the channel false |
dataEVT | Reader → application | data1Event | Set per topic: 0 or 1 | false |
dataEVT | Reader → application | data2Event | Set per topic: 0 or 1 | false |
dataEVT | Reader → application | rfidData | 0 | false |
Last Will (the DISCONNECTED form of mqttConnEVT) | Broker → application | server binding | 0 | true |
- QoS 0 (at most once): a message can be lost, and MQTT does not resend it.
- QoS 1 (at least once): the sender resends until the receiver acknowledges, so the receiver can get the same message more than once.
RETAIN is covered in Retained Messages.
Per-Topic Settings
Each entry in an endpoint's publishTopics carries its own qos and retain, and the contract requires both. The reader applies them to that topic, and it supports QoS 0 and 1. What it does with an entry that omits them is not specified in the available sources. The endpoint's qosCommon sets a QoS level for the whole endpoint, and how it interacts with the per-topic values is not specified either. In the reader's default endpoint configurations, every topic uses QoS 0 and only the Last Will topic is retained.
On a DATA1 endpoint, the reader publishes tag data at the qos of its tag-data topic. How the per-topic settings relate to the other per-message values in the table above is not specified in the available sources.
Subscriptions
Under MQTT 3.1.1, the broker delivers a message to a subscriber at the lower of the publish QoS and the QoS granted for the subscription, and it can grant less than was requested. Two consequences:
- Your application's subscriptions. Subscribe at QoS 1 to receive responses and
alertsat QoS 1. - The reader's subscription. Commands reach the reader at the QoS of its
subscribeTopics[0]entry, or lower. The contract's endpoint examples subscribe at QoS0. At QoS 0, a command can be lost between the broker and the reader.
MQTT Acknowledgments Are Not Outcomes
A PUBACK for your command means that the broker received it, not that the reader did. Only the reader's response reports what happened. When no response arrives, you can't tell whether the command was lost, is still queued, or ran and its response was lost. See MQTT Acknowledgments and Reader Responses.
Duplicates and Retries
A command can reach the reader more than once: MQTT can redeliver it, and your application can retry it. The reader handles every copy the same way:
- No duplicate detection. The reader keeps no request history, so it carries out every copy that arrives, whatever its
requestId. - The last configuration wins. For a configuration command, the copy that arrives last takes effect.
- Work in progress can refuse a repeat. If a copy arrives while the first is still in effect, some commands refuse it with a code instead of running it again.
The contract marks each command with x-iotc-idempotent:
| Marker | Meaning |
|---|---|
true | The command only reads. Repeating it changes nothing. |
false | Repeating it is not safe. Where refusal codes are listed, a repeat sent while the first is still in effect is refused with one of them. |
unknown | The command changes state, and firmware has not stated whether a repeat is harmless. Treat a repeat as a second real request. |
| Command | x-iotc-idempotent | Refusal codes for a repeat | Completion |
|---|---|---|---|
get_version | true | — | The reply is the result |
get_status | true | — | The reply is the result |
get_current_region | true | — | The reply is the result |
get_eth | true | — | The reply is the result |
get_wifi | true | — | The reply is the result |
set_wifi | false | 18 | The reply is the result |
delete_wifi_profile | unknown | — | The reply is the result |
get_endpoint_config | true | — | The reply is the result |
config_endpoint | false | 10 · 33 | The reply is the result |
get_installed_certificates | true | — | The reply is the result |
install_certificate | unknown | — | Code 1, then an alerts event with id: FILE_DOWNLOAD; read alertDetails.downloadInfo.status for DOWNLOAD SUCCESS, then SAVE SUCCESS |
delete_certificate | unknown | — | The reply is the result |
config_events | unknown | — | The reply is the result |
set_os | false | 4 | Code 1, then an alerts event with id: FIRMWARE_UPDATE; read alertDetails.fwUpdateStatus.updateStatus for FW update success |
reboot | false | 5 | Code 1; no terminal event |
get_operating_mode | true | — | The reply is the result |
set_operating_mode | false | 11 | The reply is the result |
get_post_filter | true | — | The reply is the result |
set_post_filter | unknown | — | The reply is the result |
control_operation | false | 11 | The reply is the result |
Before You Retry
- Read commands: retry freely.
- State-changing commands with no reply: read the state back first, for example with
get_endpoint_configafterconfig_endpoint. A missing reply does not mean the command did not run. - After an error code: the contract's rule is to retry only when the code's recommended action names a condition that can change without changing your request, such as waiting, stopping an inventory, or charging the reader. For other codes, change the request first. Response Codes lists each code's recommended action.
- One command at a time: the reader's command queue holds 10 commands, and commands beyond that can be lost. The contract doesn't say whether you must serialize commands to one reader; waiting for each reply before sending the next is the conservative choice.
Commands Queued While the Reader Is Offline
With the default cleanSession: true, the broker discards the reader's session when the reader reconnects, so commands published while it was offline are not delivered to it. With cleanSession: false and a stable client ID, the broker can queue QoS 1 commands for the reader, which carries them out when it reconnects. See Clean Session.
Planning for Loss
| Message | What loss means | How to plan for it |
|---|---|---|
| Command or response | You can't tell from silence what happened | Set a deadline, then read state back before a state-changing retry |
heartBeatEVT | QoS 0: a single heartbeat can be lost in transit | Decide how many missed intervals your application treats as an outage. See Reader Availability. |
mqttConnEVT | QoS 0: a transition can be missed | Confirm connection state with a command when it matters |
dataEVT | At QoS 0, which the default endpoint configurations use, tag reads can be lost | Treat the stream as a statistical view of the tag population, not a guaranteed log. See Tag Data. |
alerts | QoS 1: redelivery is possible | Handle a repeated alert without acting twice |
Tag Data
dataEVT is published at the QoS set on the data endpoint's tag-data topic, 0 or 1, and the reader's default endpoint configurations use 0. It is not retained. The contract describes the stream as a statistical view of the tag population, not a guaranteed log of every read. It says an application that needs guaranteed capture must reconcile against inventoryStatus, which heartBeatEVT carries. Reconciliation can reveal a discrepancy, but it can't prove that capture is complete: what the inventoryStatus counters count is not specified in the available sources, and post filters and read aggregation can make counts differ legitimately.
eventNum on each tag record is the only sequence signal in the stream. The contract documents no reset or wrap-around rule for it, so a gap is a reason to investigate, not proof of loss.
Related: 📕 Retention and Last Will · 📕 Correlation and Request/Response Pairing · 📕 Response Codes · 📕 dataEVT · 📙 Recover from Disconnects
Retention and Last Will
A retained message is stored by the broker and delivered to every new subscriber. A Last Will is a message that a client registers when it connects, for the broker to publish if the client's session ends without a clean disconnect. This section covers how the MQTT API uses both.
Retained Messages
| Message | RETAIN | What a late subscriber receives |
|---|---|---|
| Commands | false | Nothing. Never publish a command with RETAIN set: the broker would deliver it again whenever the reader resubscribes, and the reader would carry it out again. |
| Responses | false | Nothing |
alerts, heartBeatEVT | false | Nothing |
dataEVT | false | Nothing |
mqttConnEVT | Not settled. See Retained Connection State. | Possibly the last connection state |
| Last Will | true | The last DISCONNECTED Last Will published for the endpoint, if any |
Because almost nothing is retained, subscribe before the message you need is published. A response, event, or tag read published before your subscription is active is not delivered to you later.
Each publishTopics entry also carries a retain flag, which the contract requires; its examples set it to false on every topic except the Last Will topic. How it relates to the values above is not specified in the available sources; see Per-Topic Settings.
The retain member of a subscribeTopics entry has no effect: MQTT 3.1.1 has no retain flag on a subscription. The contract nonetheless requires it, and its own examples omit it.
Retained Connection State
The contract does not settle whether mqttConnEVT is retained. Its operation declares RETAIN true, and its channel declares false; each cites a different Zebra source, and the question is open with the device owners. The RFD40/RFD90 IoT API specification also records both the connected message and the Last Will as retained.
Until it is settled, write subscribers that work either way:
- Don't wait for a retained message. If none arrives when you subscribe, the reader's state is unknown, not disconnected.
- Don't trust a retained message as current. A retained
DISCONNECTEDLast Will has no timestamp, so you can't tell how old it is.
Last Will
| Property | Value |
|---|---|
| Registered by | The reader, in its CONNECT packet, for each endpoint session |
| Published by | The broker |
| Published when | The session ends without a clean MQTT disconnect, including when the broker receives nothing from the reader for one and a half keep-alive intervals |
| Suppressed by | A clean MQTT disconnect |
| Payload | The DISCONNECTED form of mqttConnEVT, without a timestamp |
| Topic | The endpoint's publishTopics[2] topic. The contract's server binding shows <tenantId>/MGMT/clients/rfid/<serial> for a management endpoint. |
| QoS and RETAIN | QoS 0, RETAIN true |
| Configurable | No. The endpoint configuration has no Last Will fields. |
What the Last Will does and does not tell you:
- Its arrival means the broker detected that the session ended without a clean disconnect. Because detection can depend on the keep-alive interval, it can arrive some time after the connection was lost.
- Its absence means nothing. A clean disconnect suppresses the Last Will, and whether the reader reports a clean disconnect in any other way is not specified in the available sources.
- Whether the reader registers a Last Will for an endpoint without a
publishTopics[2]entry is not specified in the available sources.
Reader Availability
No single message proves that a reader is available. Combine these signals:
| Signal | What it shows |
|---|---|
heartBeatEVT arriving at its interval | The reader and the path to your application are working. This is the primary liveness signal. |
heartBeatEVT no longer arriving | The reader, its session, or the path to your application needs attention |
A CONNECTED mqttConnEVT | A new session was established for that endpoint |
A DISCONNECTED mqttConnEVT | The session ended without a clean disconnect |
| A reply to a command | The endpoint that carried the command is connected and processing commands |
The reader publishes heartBeatEVT at the configured interval, at most 65,535 seconds, while the heartbeat is enabled with config_events; a reboot applies the setting. heartBeatEVT is published at QoS 0, so the MQTT layer does not resend a heartbeat lost in transit. Choose how many missed intervals your application treats as an outage.
Related: 📕 Delivery and Retry Semantics · 📕 Session Lifecycle · 📕 mqttConnEVT · 📕 heartBeatEVT · 📕 MQTT Connectivity