Imagine managing hundreds or thousands of IoT devices from a centralized platform.
An operator opens a dashboard, selects a device, and clicks Restart Device.
The backend publishes a command to an MQTT broker. The broker successfully delivers the message, and the application receives a successful publishing response.
The dashboard displays:
Command Sent Successfully.
But a few seconds later, the device is still online. It hasn't restarted.
What happened?
The MQTT broker did its job. The device, however, never completed the requested operation.
This highlights an important architectural distinction in IoT development:
Successful message delivery does not necessarily mean successful command execution.
When building real-world IoT platforms, understanding this difference is essential for designing reliable device communication, command tracking, and backend systems.
Let's explore how to approach this problem.
1. Understanding What MQTT Actually Guarantees
MQTT is a lightweight messaging protocol designed for communication between devices and applications.
One of its most important features is Quality of Service (QoS), which defines the delivery guarantees for MQTT messages.
There are three QoS levels:
| QoS | Delivery guarantee | Practical consideration |
|---|
| QoS 0 | At most once | Messages may be lost |
| QoS 1 | At least once | Duplicate delivery is possible |
| QoS 2 | Exactly once | Exactly-once delivery within the MQTT protocol exchange |
These guarantees are useful, but they are frequently misunderstood.
Consider QoS 1.
When a publisher sends a message, the broker acknowledges its receipt. Depending on the subscription and delivery flow, the receiving client also participates in the acknowledgement process.
However, none of this automatically confirms that the device successfully executed the business operation represented by the message.
For example:
- The broker receives a restart command.
- The device receives the MQTT message.
- The device acknowledges the protocol exchange.
- The device encounters an internal error before restarting.
From the messaging perspective, delivery may have succeeded.
From the application perspective, the operation failed.
Even QoS 2 does not solve this problem. Its exactly-once guarantee applies to the MQTT protocol exchange, not to the execution of arbitrary application logic or physical operations.
This is the first distinction every IoT developer should understand.
2. Why MQTT Acknowledgements Are Not Enough
Let's consider a realistic device-management scenario.
An operator sends a configuration update to a remote device.
The communication flow might look like this:
- The frontend sends a request to a FastAPI backend.
- FastAPI validates the request.
- The backend publishes a configuration command to MQTT.
- The broker accepts the message.
- The device receives the message.
- The device attempts to apply the configuration.
A basic implementation might look like this:
import json
@app.post("/devices/{device_id}/restart")
async def restart_device(device_id: str):
payload = {
"device_id": device_id,
"command": "RESTART"
}
await mqtt_client.publish(
f"devices/{device_id}/commands",
json.dumps(payload),
qos=1
)
return {
"success": True,
"message": "Restart command published"
}
There is nothing inherently wrong with publishing a command this way.
The problem is what the API response communicates.
If the backend returns "success": True, developers or frontend applications may interpret that as successful device execution.
But the backend has only confirmed that the publishing operation completed according to the client's implementation and acknowledgement behavior.
It has not confirmed that the device performed the restart.
A better response would explicitly distinguish the command's publication status from its execution status.
For example:
{
"command_id": "cmd-10245",
"device_id": "device-101",
"status": "PUBLISHED",
"message": "Command published. Awaiting device acknowledgement."
}
This is a much more accurate representation of the operation.
3. Designing an Application-Level Command Lifecycle
A reliable IoT platform should treat device commands as trackable business operations rather than simple MQTT messages.
One practical approach is to introduce a command lifecycle.
For example:
CREATED
|
v
PUBLISHED
|
v
RECEIVED
|
v
ACCEPTED
|
v
EXECUTING
|
+-------> SUCCEEDED
|
+-------> FAILED
Other possible outcomes:
TIMED_OUT
EXPIRED
Each state represents a different stage of the operation.
| Status | Meaning |
|---|
| CREATED | Command registered in the backend |
| PUBLISHED | Broker publication confirmed |
| RECEIVED | Device reports receiving the command |
| ACCEPTED | Device validates and accepts the command |
| EXECUTING | Device has started the operation |
| SUCCEEDED | Device reports successful execution |
| FAILED | Device reports execution failure |
| TIMED_OUT | Expected acknowledgement did not arrive within the defined period |
| EXPIRED | Command is no longer valid for execution |
An important design consideration is that these states should not be treated as interchangeable.
For example, PUBLISHED should not automatically transition to SUCCEEDED.
Similarly, a timeout does not always mean that the device failed. The acknowledgement might have been lost even though the operation completed.
That distinction becomes especially important when building dashboards and automated recovery workflows.
Every command should have a unique identifier
A command identifier helps the backend associate a device response with the original request.
Consider this payload:
{
"command_id": "cmd-10245",
"correlation_id": "req-78120",
"device_id": "device-101",
"command": "RESTART",
"issued_at": "2026-11-11T09:00:00Z",
"expires_at": "2026-11-11T09:05:00Z",
"parameters": {}
}
Here:
command_id uniquely identifies the operation.correlation_id connects it to the originating API request or workflow.device_id identifies the target.issued_at records when the command was created.expires_at defines when the command becomes invalid.
The backend can now maintain a clear relationship between the original request, MQTT publication, device acknowledgement, and final execution result.
4. Implementing Reliable Command Tracking With FastAPI
Let's look at how this can be implemented in a backend application.
First, we need a database model to track commands.
A simplified SQLAlchemy model could look like this:
import uuid
from sqlalchemy import (
Column,
String,
DateTime,
JSON,
Integer,
func
)
from sqlalchemy.dialects.postgresql import UUID
from database import Base
class DeviceCommand(Base):
__tablename__ = "device_commands"
id = Column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4
)
device_id = Column(
String,
nullable=False,
index=True
)
command_type = Column(
String,
nullable=False
)
status = Column(
String,
nullable=False,
default="CREATED"
)
payload = Column(
JSON,
nullable=False
)
attempts = Column(
Integer,
nullable=False,
default=0
)
created_at = Column(
DateTime(timezone=True),
server_default=func.now()
)
completed_at = Column(
DateTime(timezone=True),
nullable=True
)
This model provides a persistent record of every command sent to a device.
Now let's create an API endpoint.
from fastapi import APIRouter, HTTPException
from uuid import uuid4
router = APIRouter()
@router.post("/devices/{device_id}/commands")
async def send_device_command(
device_id: str,
command: str
):
command_id = str(uuid4())
payload = {
"command_id": command_id,
"device_id": device_id,
"command": command,
"parameters": {}
}
# 1. Persist command as CREATED.
# 2. Publish through MQTT.
# 3. Wait for broker publisher confirmation.
# 4. Update status to PUBLISHED.
return {
"command_id": command_id,
"device_id": device_id,
"status": "PUBLISHED"
}
The omitted database and MQTT operations are deliberate placeholders; a production implementation must persist the command before publication and only mark it PUBLISHED after the configured broker confirmation succeeds.
For stronger reliability, command creation and an outbox event can be committed in the same PostgreSQL transaction. A background publisher can then publish the command without losing the intent if the broker is temporarily unavailable.
This also prevents the API from becoming tightly coupled to the availability of the MQTT broker.
Receiving device acknowledgements
The device should publish an application-level response after processing the command.
For example:
{
"command_id": "cmd-10245",
"device_id": "device-101",
"status": "SUCCEEDED",
"completed_at": "2026-11-11T09:01:12Z",
"result": {
"restart_completed": true
}
}
A backend MQTT consumer listens to the acknowledgement topic:
async def handle_device_acknowledgement(
message: dict,
session
):
command_id = message.get("command_id")
command = (
session.query(DeviceCommand)
.filter(
DeviceCommand.id == command_id
)
.with_for_update()
.first()
)
if not command:
return
if command.status in {
"SUCCEEDED",
"FAILED",
"EXPIRED"
}:
return
if message["device_id"] != command.device_id:
return
command.status = message["status"]
session.commit()
This is an illustrative starting point. A real implementation should validate device identity and authorization, validate allowed state transitions, handle malformed or delayed acknowledgements, and commit updates transactionally.
The backend now has an application-level confirmation instead of relying only on MQTT publication status.
5. Handling Real-World Device Failures
Implementing command tracking is only the beginning.
Production IoT systems must account for situations where devices and networks behave unpredictably.
Here are some important cases.
Scenario A: Device receives the same command twice
With QoS 1, duplicate message delivery is possible.
Imagine a device receives the same restart command twice.
Without duplicate protection, it might execute the operation multiple times.
The solution is application-level idempotency.
The device should maintain processed command identifiers and reject or safely acknowledge commands it has already handled.
For operations involving persistent device state, the deduplication record should survive reboots where necessary.
Scenario B: Device disconnects during execution
Suppose a device receives a configuration update and disconnects before publishing its acknowledgement.
Did the configuration update succeed?
The backend cannot safely assume either outcome.
It should keep the command in an uncertain or pending state until it receives a valid acknowledgement or performs a reconciliation process.
For critical configuration updates, the device can report its actual configuration after reconnecting.
Scenario C: An old command arrives late
Imagine an operator sends a configuration command, but the device remains offline for several hours.
When it reconnects, should it execute that old command?
Not necessarily.
Commands should have an application-level expiry timestamp.
The device should compare the current time against the expiry and reject commands that are no longer valid.
MQTT 5 also provides Message Expiry Interval, which can help limit how long a message remains available for delivery.
However, broker-side expiry does not replace device-side validation, particularly when messages have already been delivered or persisted elsewhere.
Scenario D: The backend times out, but the device completed the operation
This is another important distributed-systems problem.
The backend waits for an acknowledgement, but the network drops the response.
The device successfully performs the operation, while the backend marks it TIMED_OUT.
Automatically retrying the same command might cause unintended behavior.
For this reason, timeouts should be treated as uncertain outcomes, not automatic proof of failure.
A reconciliation mechanism can compare the desired state with the device's reported state before deciding whether another command is necessary.
6. Building a More Reliable IoT Command Architecture
A production-ready command architecture should separate command creation, message delivery, device execution, and state verification.
A simplified architecture looks like this:
Command flow:
FastAPI → PostgreSQL → Command Publisher → MQTT Broker → IoT Device
Acknowledgement flow:
IoT Device → MQTT Broker → Acknowledgement Consumer → PostgreSQL → Dashboard
The database acts as the source of truth for command tracking, while the device's reported state provides evidence of its actual condition.
For larger device fleets, additional considerations become important:
1. Command expiry
Prevent stale commands from being executed long after they were requested.
2. Idempotent execution
Ensure duplicate delivery does not cause repeated unintended operations.
3. Retry policies
Use controlled retries with backoff, and distinguish between retryable failures and permanent failures.
4. Device authentication
Never trust an acknowledgement solely because it contains a valid command ID. Authenticate the publishing device and verify that it is authorized to acknowledge the targeted command.
5. Observability
Track command latency, failure rates, acknowledgement timeouts, retry counts, and device execution outcomes.
6. Desired versus reported state
For configuration management, maintain the configuration the backend wants the device to have separately from the configuration the device reports it currently has.
This allows the backend to identify configuration drift and initiate reconciliation when appropriate.
It is also important to remember that MQTT retained messages, persistent sessions, and offline message queues have different delivery semantics. They should be configured according to the command's purpose. A retained restart command, for example, can be dangerous if a device reconnects much later and receives an outdated instruction.
Conclusion
A successful MQTT publication does not necessarily mean a device has successfully executed a command.
The broker can guarantee message delivery according to the selected QoS level, but application-level execution requires additional engineering.
By introducing unique command identifiers, persistent command tracking, application-level acknowledgements, idempotency, expiry handling, and state reconciliation, developers can build much more reliable IoT communication systems.
The most important lesson is simple:
Message delivery is a communication guarantee. Successful execution is a business guarantee. They are not the same thing.
When building an IoT platform, don't just ask whether the broker delivered the message.
Ask whether the device received it, accepted it, executed it, and reached the expected state.
That is where reliable IoT engineering really begins.
Technologies discussed: MQTT, MQTT QoS, Python, FastAPI, PostgreSQL, SQLAlchemy, Event-Driven Architecture, IoT Device Management, Distributed Systems.
Suggested C# Corner tags: IoT Development, Python, Backend Development, MQTT, Software Architecture, Distributed Systems.
Arun PottiPosted Nov 11, 2026, 5:12 AM
Great Article.