Digital Twin

Digital Twin State Synchronization: Reported, Desired and Conflict Patterns

How to keep a digital twin and its physical asset in sync: reported and desired state, version numbers, conflict handling, reconnect recovery and retained MQTT topics, with a working Python device.

Digital twin state synchronization diagram with desired and reported state documents flowing between an app, an MQTT broker and a device
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Digital twin state synchronization is the problem of keeping what the twin believes and what the asset actually is in agreement, even across disconnects, delays and conflicting commands. This article explains the reported vs desired state model, versioning and conflict handling, and how to implement it with retained MQTT topics, ending with a working Python device that converges to desired state.

For the broader context, start with the digital twin technology guide.

Why Synchronization Is Hard

A twin is only useful if it is right. In practice, several things conspire against that:

  • Latency: the twin sees a value some milliseconds or seconds after it changed.
  • Disconnects: a device goes offline and comes back with a different state.
  • Concurrent writers: an operator and an automation both change a setpoint.
  • Ordering: messages from different paths arrive out of order.
  • Partial failure: a command is delivered but the acknowledgement is lost.

Good synchronization design does not eliminate these problems; it makes them visible and recoverable.

The Reported and Desired State Model

The most widely used pattern, popularised by device shadow services and twin frameworks such as Eclipse Ditto, splits state into two documents:

Document Written by Meaning
Reported The device only What the asset actually is right now
Desired Apps, operators, automations What the asset should become
Delta (derived) Computed Fields where desired differs from reported

The golden rule: only the device writes reported state. An app that wants a pump at 1,450 rpm writes desired.speed = 1450. The device sees the delta, acts, and then reports reported.speed = 1450. The twin UI shows both, so operators can distinguish "requested" from "achieved".

This separation removes a whole class of bugs where the UI shows a value the machine never actually reached. Compare how lightweight shadows and full twins apply this in device shadow vs digital twin.

Implementing Digital Twin State Synchronization with MQTT

MQTT maps cleanly onto this model using retained topics:

connectmq/demo/<id>/plant1/pump7/state/reported   retained, written by device
connectmq/demo/<id>/plant1/pump7/state/desired    retained, written by apps
connectmq/demo/<id>/plant1/pump7/status           retained, last will "offline"

Because both state topics are retained, the broker delivers the latest version to anyone who subscribes:

  • A viewer opening the twin gets reported and desired state immediately.
  • A device that reconnects after an outage receives the latest desired state immediately and can converge, even if the desired change was made while it was offline.

That second property is the key to robust synchronization. You do not need a separate "pending commands" store for setpoint-style changes; the retained desired document is the pending command. See digital twin retained MQTT for a full tutorial on retained twin state.

Desired State vs Commands

Use desired state for idempotent targets: setpoints, modes, configuration, on/off. Applying them twice has the same effect as once. Use non-retained commands for actions: reset a counter, open a door for five seconds, start a calibration. Retaining an action command would replay it after every reconnect, which is dangerous.

Versioning and Conflict Handling

Every state document should carry a version and a timestamp:

{
  "version": 42,
  "ts": 1792152000000,
  "source": "operator:maria",
  "speed_rpm": 1450,
  "mode": "AUTO"
}

Use them to handle conflicts:

  1. Monotonic versions on desired state. Writers read the current desired document, increment the version and publish. Consumers ignore any desired message with a version lower than the one already applied.
  2. Reported state echoes the applied desired version. The device includes desired_version: 42 in its reported document, so the twin knows which request has been satisfied.
  3. Last writer wins, made explicit. For most setpoints, the most recent version wins. Record source so the UI can show who changed it.
  4. Field-level merges where needed. If one app owns speed and another owns mode, merge at field level instead of replacing the whole document, or split them into separate retained topics.
  5. Device-side guards. The device rejects values outside safe limits and reports the rejection with a reason, rather than silently ignoring them.

Brokers do not enforce versions for you; this is logic in your publishers and the device. MQTT 5 user properties are a convenient place to carry version metadata without touching the payload.

Handling Reconnects and Stale State

When a device reconnects, follow a fixed sequence:

  1. Connect with a last will on the status topic.
  2. Publish online to the status topic, retained.
  3. Publish the full current reported state, retained, from local sensors, not from memory of what it last sent.
  4. Subscribe to the desired topic and receive the retained desired document.
  5. Compute the delta, apply it and publish updated reported state.

Step 3 before step 4 matters: the twin first learns the truth, then the device converges toward the target. If you reverse them, a viewer might briefly see stale reported state labelled as current.

On the twin side, treat reported state as stale when the status topic says offline. Grey it out rather than showing old values as live.

A Working Device That Converges to Desired State

This Python device uses the public broker and paho-mqtt 2.x. Replace 9c4e with your own random prefix.

import json
import time
import paho.mqtt.client as mqtt

BASE = "connectmq/demo/9c4e/plant1/pump7"
LIMITS = {"speed_rpm": (0, 3000)}

reported = {"version": 0, "desired_version": 0, "speed_rpm": 0, "mode": "MANUAL"}

def publish_reported(client):
    reported["version"] += 1
    reported["ts"] = int(time.time() * 1000)
    client.publish(f"{BASE}/state/reported", json.dumps(reported), qos=1, retain=True)

def on_connect(client, userdata, flags, reason_code, properties):
    client.publish(f"{BASE}/status", "online", qos=1, retain=True)
    publish_reported(client)
    client.subscribe(f"{BASE}/state/desired", qos=1)

def on_message(client, userdata, msg):
    if not msg.payload:
        return
    desired = json.loads(msg.payload)
    if desired.get("version", 0) <= reported["desired_version"]:
        return
    errors = []
    for key, value in desired.items():
        if key in ("version", "ts", "source"):
            continue
        low, high = LIMITS.get(key, (None, None))
        if low is not None and not (low <= value <= high):
            errors.append(f"{key} out of range")
            continue
        reported[key] = value
    reported["desired_version"] = desired["version"]
    reported["errors"] = errors
    publish_reported(client)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="pump7-9c4e")
client.will_set(f"{BASE}/status", "offline", qos=1, retain=True)
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883)
client.loop_forever()

Set a desired state from another terminal:

mosquitto_pub -h broker.connectmq.com -p 1883 -i pump7-ops-9c4e -q 1 -r \
  -t "connectmq/demo/9c4e/plant1/pump7/state/desired" \
  -m '{"version": 1, "source": "operator", "speed_rpm": 1450, "mode": "AUTO"}'

Stop the device, publish version 2 with a new speed, then start the device again. It receives the retained desired document on subscribe and converges immediately. Watch it all with mosquitto_sub -t "connectmq/demo/9c4e/plant1/pump7/#" -v.

Showing Synchronization State in the Twin UI

Synchronization logic is only half the job; operators need to see it. A good twin interface makes the sync state explicit rather than hiding it:

  • Pending: desired differs from reported and the device is online. Show the target value next to the actual one, for example "1,450 rpm requested, 1,200 rpm actual".
  • Applied: reported echoes the latest desired version. Show only the actual value.
  • Rejected: the device reported an error for that version. Show the reason and keep the previous value.
  • Stale: the status topic says offline. Grey out values and show when they were last updated.

Add a timeout as well. If a desired change remains pending for longer than the device normally needs, flag it so someone investigates instead of assuming the machine complied. These states fall directly out of the versioned reported and desired documents described above, so they need no extra infrastructure.

State Synchronization Checklist

  1. Reported state written only by the device, desired state only by apps.
  2. Both documents retained, with version and timestamp.
  3. Reported echoes the last applied desired version.
  4. Actions sent as non-retained commands with acknowledgements.
  5. Last will on a retained status topic.
  6. Device publishes reported before applying desired on reconnect.
  7. Device validates limits and reports rejections.
  8. UI shows desired vs reported and marks offline twins as stale.

Synchronize Your Digital Twins on ConnectMQ

ConnectMQ is a fully managed MQTT service supporting retained messages, last will, QoS 0 to 2 depending on plan, and MQTT 5 features such as user properties, message expiry and session expiry. The portal's live topic explorer makes it easy to inspect reported and desired documents side by side. Your synchronization logic lives in devices and apps connecting as regular MQTT clients, while the MQTT broker in the cloud handles delivery. Start free on the Serverless plan at app.connectmq.com, no credit card needed.

Frequently Asked Questions

What is the difference between reported and desired state?

Reported state is what the device actually is, written only by the device. Desired state is what apps want it to become. The difference between them is the delta the device must act on.

Why use retained messages for desired state?

A retained desired document is delivered to a device as soon as it subscribes, so changes made while it was offline are applied automatically on reconnect.

How do I resolve conflicting updates to a digital twin?

Add versions and sources to desired documents, ignore older versions, merge at field level where ownership differs, and let the device validate and report what it actually applied.

Should commands be retained?

No. Retain idempotent desired state only. Action commands should be non-retained, ideally with message expiry and an acknowledgement.

Related articles

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.