MQTT Delivery Is Not Device Execution
“Delivered” is an ambiguous status unless the system explains what was delivered, to whom, and at which layer.
MQTT QoS governs message delivery between protocol peers. A successful publisher-to-broker QoS 1 exchange does not prove that the subscribing device executed the command. The broker-to-device delivery is a separate exchange. QoS 2 likewise does not make a physical action an exactly-once business transaction. See the OASIS MQTT specification, Section 4.3 (https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901234).
A command can pass through several distinct checkpoints:
- The backend records the request.
- The publisher submits the message.
- The broker handles the publication.
- The device application validates the command.
- The device attempts the operation.
- The device verifies and reports the outcome.
Your interface should display only the strongest conclusion supported by the available evidence. If the backend knows only that it published a message, “Command sent” is more accurate than “Completed.”
Also check what your MQTT library means by a successful publish callback. Its meaning can depend on the client implementation and selected QoS.
Define What Counts as Success
Before designing acknowledgement messages, define a completion condition for each command.
For a configuration update, success might mean that the device persisted the new setting and read it back. For a motor command, it might require position feedback. For a print request, sending bytes to a printer may establish submission without proving that paper was printed.
These are different product guarantees.
A useful command specification answers three questions:
- What must the device validate before accepting the request?
- What evidence establishes completion?
- What should the system report when that evidence is unavailable?
If the hardware cannot verify a physical result, report the narrower outcome it can establish. For example, use “Submitted to printer” instead of “Printed.”
Give Every Logical Command a Stable Identity
Every command should carry an application-level identifier that remains stable across retries.
Consider this illustrative request:
{
"schemaVersion": 1,
"commandId": "cmd_7f93a2",
"deviceId": "controller-042",
"type": "set_output",
"parameters": {
"channel": 1,
"enabled": true
},
"issuedAt": "2026-09-11T08:00:00Z",
"expiresAt": "2026-09-11T08:00:30Z"
}
The fields have separate responsibilities:
schemaVersionidentifies the payload contract.commandIdconnects requests, retries, responses, and logs.deviceIdidentifies the intended target.typeandparametersdescribe the operation.issuedAtsupports traceability.expiresAtdefines the latest permitted start time in this example.
A retry of the same logical request should reuse its commandId. A new user action should receive a new identifier.
Do not reuse an identifier with changed parameters. Store a payload fingerprint alongside the command record and reject conflicting reuse.
Expiration also needs an explicit clock policy. If devices cannot trust their wall clocks, define how they establish time or reject time-sensitive commands. In this contract, expiration prevents a late start; it does not automatically cancel an operation already underway.
Separate Acceptance from Completion
A compact application-level lifecycle can use these states:
accepted: validation passed and the device recorded the command for processing.executing: the operation started.succeeded: the defined completion condition was verified.rejected: validation failed and execution did not begin.failed: execution was attempted but did not meet the completion condition.
These names are a proposed application contract, not MQTT protocol statuses.
For operations that need reboot recovery, emit accepted only after the command has been recorded durably.
An acceptance response might look like this:
{
"schemaVersion": 1,
"commandId": "cmd_7f93a2",
"deviceId": "controller-042",
"status": "accepted",
"statusVersion": 1
}
A completion response should include evidence appropriate to the operation:
{
"schemaVersion": 1,
"commandId": "cmd_7f93a2",
"deviceId": "controller-042",
"status": "succeeded",
"statusVersion": 3,
"result": {
"channel": 1,
"enabled": true,
"verification": "output_register_readback"
}
}
Here, success means that the output register was read back successfully. It does not independently prove that an attached machine moved or performed its intended task.
Use a monotonically increasing statusVersion per command so the backend can ignore duplicate or older updates. Preserve this sequence across recovery if the command survives a restart.
Design the Response Path Before Sending Commands
A simple topic structure is:
devices/controller-042/commands
devices/controller-042/command-results
The device subscribes to its command topic and publishes status updates to its result topic. The backend correlates responses using commandId.
Before publishing a command, the backend should persist its record and establish the response subscription. Otherwise, a fast response may arrive before the system is ready to process it.
Restrict topic access so a device can receive only its authorized commands and publish only its own results. Validate the relationship between the authenticated publisher, topic, and payload identity.
MQTT 5 provides Response Topic and Correlation Data properties for request-response patterns. These can carry routing and correlation information, but the application still defines the result payload and success criteria. See MQTT 5, Section 4.10 (https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html).
For MQTT 3.1.1 deployments, the explicit topic and payload convention above provides an application-level alternative.
Treat a Timeout as an Unknown Outcome
A missing response does not establish that nothing happened.
The command may never have reached the device. The device may still be executing it. Or the operation may have completed just before the connection failed.
Use separate deadlines for different expectations:
- Acceptance deadline: how long to wait for the device to accept or reject the request.
- Completion deadline: how long the operation may take after acceptance.
- Command expiration: how late the device may begin the operation.
Choose these values from measured device behavior and product requirements.
When a deadline passes without sufficient evidence, record an observation such as outcome_unknown. Do not manufacture a device-reported failure.
The interface can show:
Confirmation unavailable. Checking device status.
A timeout is not a cancellation. If cancellation is required, design it as a separate request with its own acceptance rules and result.
Keep timed-out commands available for reconciliation. A later valid result should update the record while preserving the fact that confirmation arrived late.
Make Retries Safe Before Enabling Them
Application retries can repeat a physical action unless the device recognizes the original command.
A device-side command journal should record the command identity, payload fingerprint, execution state, and final result. When a duplicate arrives:
- Return the stored result if the command is complete.
- Return the current state if it is still running.
- Reject the request if the same identifier carries different content.
Serialize processing or reserve the command identity atomically so two simultaneous deliveries cannot both start execution.
Retain deduplication records for at least the supported retry and delayed-delivery window.
Prefer explicit target-state commands where practical. “Set output to enabled” is easier to reconcile than “Toggle output,” because repetition does not intentionally invert the state.
However, a journal alone cannot guarantee exactly-once physical execution. A device can lose power after performing an action but before saving its result. For dispensing, printing, or similar operations, recovery may require hardware transaction identifiers, sensor evidence, or operator review.
If the outcome is ambiguous and duplication would matter, reconcile before retrying.
Prevent Old Commands from Executing Later
One-shot actions should generally be published without retention. A retained MQTT message can be delivered to a later subscriber, which makes stale action requests particularly problematic. MQTT 5 Message Expiry Interval limits message lifetime in the messaging system; it does not replace device-side execution rules. See the MQTT 5 specification (https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html).
Check expiration before starting a queued command, including after reconnect or reboot.
Separate one-shot actions from desired-state synchronization. A stored desired configuration may be intentional, but it needs versioning and reconciliation rules of its own.
Likewise, a heartbeat is not command-completion evidence. A device can be online while its actuator is blocked or its command processor is unavailable.
Test the Failures That Create False Success
Test the command contract across backend, broker, firmware, and hardware boundaries.
Include these scenarios:
- Device disconnected before delivery: the interface never shows unverified success.
- Invalid parameters: the device returns
rejectedwithout operating hardware. - Duplicate command during execution: only one execution starts.
- Same identifier with changed parameters: the device rejects the conflict.
- Completion response lost: reconciliation recovers the result without repeating the action.
- Reboot after acceptance: the device recovers the recorded command according to policy.
- Power loss after physical action: the system preserves uncertainty until evidence resolves it.
- Expired command after reconnect: the device does not start the stale operation.
- Older status arriving late: the backend does not regress from completion to acceptance.
- Result published under another device’s identity: access controls or validation reject it.
For each test, inspect both the software status and the actual hardware outcome. A dashboard alone cannot demonstrate that duplicate physical execution was prevented.
Build a Traceable Path from Request to Result
A reliable command workflow needs an explicit chain of evidence: who requested the operation, which device accepted it, what happened during execution, and how the outcome was verified.
Start with one command type. Define its completion condition, add a stable identifier, implement acceptance and result messages, and test what happens when confirmation disappears. Expand the pattern only after its retry and recovery behavior is understood.
YUNJI’s MQTT Cloud Integration service (https://yunji-node.com/solutions/mqtt-cloud-integration) covers device identity, command workflows, state synchronization, and failure handling across connected-product systems. When preparing an integration review, bring your topic structure, sample payloads, firmware behavior, and logs from a command whose result was unclear.



