Digital Twin

Digital Twin Retained MQTT Tutorial: Using Retained Messages as Twin State

Build a lightweight digital twin where every property is a retained MQTT topic. Viewers get full twin state the instant they subscribe, with no database. Includes a Python simulator, a Node.js viewer and cleanup commands.

Digital twin retained MQTT tutorial showing per-property retained topics loading full machine state into a viewer instantly
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

Using digital twin retained MQTT messages is the lightest way to give a twin memory: each property lives on its own retained topic, so any viewer that subscribes receives the complete current state instantly, without a database or API. In this tutorial you will build a small machine twin with a Python simulator, a Node.js viewer that rebuilds full state on connect, and the commands you need to inspect and clean up retained topics.

For the wider concept, see the digital twin technology guide.

How Retained Messages Work

Normally an MQTT broker forwards a message to current subscribers and forgets it. When a publisher sets the retain flag, the broker also stores that message as the last known value for the topic. Any client that later subscribes to a matching topic receives the stored message immediately, marked as retained.

Key rules:

  • The broker keeps one retained message per topic: the latest one replaces the previous.
  • Wildcard subscriptions receive the retained message of every matching topic.
  • Publishing an empty payload with the retain flag deletes the retained message.
  • Retained messages are delivered at the lower of the published and subscribed QoS.

That combination is exactly what a twin needs: a key-value store of current state, addressed by topic, readable by wildcard. For an overview of where this fits among other state approaches, see digital twin architecture.

Designing the Topic Tree: One Property per Topic

You can retain one JSON document per asset or one value per property. This tutorial uses one topic per property, which has real advantages:

Approach Pros Cons
One retained JSON per asset Fewer messages, atomic snapshot Every change resends all fields; concurrent writers overwrite each other
One retained topic per property Small updates, independent writers, easy subscriptions to a single value More topics; no atomic snapshot across properties

For a CNC machine on the public broker, the tree looks like this (keep within 8 topic levels):

connectmq/demo/f0c3/cell1/cnc1/spindle_rpm
connectmq/demo/f0c3/cell1/cnc1/feed_mm_min
connectmq/demo/f0c3/cell1/cnc1/tool
connectmq/demo/f0c3/cell1/cnc1/mode
connectmq/demo/f0c3/cell1/cnc1/alarm
connectmq/demo/f0c3/cell1/cnc1/status

Payloads are plain values, optionally with a timestamp. Here we use a small JSON object {"v": 8000, "ts": 1792152000000} so viewers know how fresh each value is.

Replace f0c3 with your own random string. The public MQTT test broker is readable by everyone, so publish only demo data.

Step 1: Publish Twin Properties as Retained Messages

This Python simulator publishes each property only when it changes, always with retain=True, and uses a last will to mark the machine offline:

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

BASE = "connectmq/demo/f0c3/cell1/cnc1"
last_sent = {}

def publish_property(client, name, value):
    if last_sent.get(name) == value:
        return
    payload = json.dumps({"v": value, "ts": int(time.time() * 1000)})
    client.publish(f"{BASE}/{name}", payload, qos=1, retain=True)
    last_sent[name] = value

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="cnc1-twin-f0c3")
client.will_set(f"{BASE}/status", json.dumps({"v": "offline"}), qos=1, retain=True)
client.connect("broker.connectmq.com", 1883)
client.loop_start()

publish_property(client, "status", "online")
publish_property(client, "tool", 3)

try:
    while True:
        running = random.random() > 0.1
        publish_property(client, "mode", "AUTO" if running else "HOLD")
        publish_property(client, "spindle_rpm", random.choice([7950, 8000, 8050]) if running else 0)
        publish_property(client, "feed_mm_min", 1000 if running else 0)
        publish_property(client, "alarm", None if running else "E-STOP")
        time.sleep(2)
except KeyboardInterrupt:
    publish_property(client, "status", "offline")
    time.sleep(0.5)
    client.loop_stop()
    client.disconnect()

Publishing on change rather than on a timer keeps traffic low and respects the public broker's rate limit of 10 messages per second per client. Retained state guarantees viewers still see unchanged values.

Step 2: Build a Viewer That Loads Full State Instantly

The viewer subscribes with a wildcard, collects retained messages into a twin object, then keeps applying live updates. With MQTT.js in Node.js:

const mqtt = require("mqtt");

const base = "connectmq/demo/f0c3/cell1/cnc1";
const twin = {};

const client = mqtt.connect("mqtt://broker.connectmq.com:1883", {
  clientId: "cnc1-viewer-" + Math.random().toString(16).slice(2, 10),
  protocolVersion: 5,
});

client.on("connect", () => {
  client.subscribe(`${base}/+`, { qos: 1 });
});

client.on("message", (topic, payload, packet) => {
  const property = topic.slice(base.length + 1);
  if (payload.length === 0) {
    delete twin[property];
    return;
  }
  const { v, ts } = JSON.parse(payload.toString());
  twin[property] = { value: v, ts, fromRetained: packet.retain };
  console.clear();
  console.table(twin);
});

Run the viewer after the simulator has started. The table fills with every property at once, because each retained message is delivered on subscribe with packet.retain set to true. Subsequent updates arrive as live messages. In a browser, the same code works with wss://broker.connectmq.com:8084/mqtt, which is how 3D twin viewers load a whole scene in one round trip; digital twin with MQTT shows the browser variant.

Step 3: Inspect and Clean Up Retained Topics

Retained messages stay until replaced or cleared, which is a feature until you rename a property. Inspect everything under your prefix:

mosquitto_sub -h broker.connectmq.com -p 1883 -i inspect-f0c3 \
  -t "connectmq/demo/f0c3/#" -v

Clear a property you no longer use by publishing an empty retained message:

mosquitto_pub -h broker.connectmq.com -p 1883 -i cleanup-f0c3 \
  -t "connectmq/demo/f0c3/cell1/cnc1/alarm" -r -n

The -n flag sends a null (zero-length) payload; together with -r it removes the retained message.

Scaling the Pattern to Many Assets

One machine is easy; a whole plant needs a few extra conventions. Keep the same property names across assets of the same type, so a viewer can render any CNC from the same template. Put the asset hierarchy in the topic path, for example site/area/line/asset/property, so viewers can subscribe to exactly the branch they display: a line view subscribes to .../line2/+/+, a single-machine page to .../line2/cnc4/+. On the public broker, remember the limit of 8 topic levels and 10 subscriptions per client, which is plenty for a demo but a reason to use your own broker for real deployments.

Viewers that open a large branch receive one retained message per property at once. That is usually fine, but parse and apply them in a batch before the first render, rather than re-rendering the scene for every single message. The result is a twin that appears complete on the first frame.

MQTT 5 Options That Improve Retained Twins

MQTT 5 adds controls that make retained state more precise:

  • Message expiry interval: set an expiry on volatile properties so a retained value disappears if it is not refreshed, rather than showing a stale reading forever.
  • Retain handling on subscribe: 0 sends retained messages on every subscribe, 1 only for new subscriptions, 2 never. Use 2 for consumers that only want live changes.
  • Retain as published: lets a bridge or relay see whether the original message was retained.

In MQTT.js, a subscription with { qos: 1, rh: 2 } skips retained messages, and publishing with { retain: true, properties: { messageExpiryInterval: 300 } } makes a value expire after five minutes. See MQTT 5 features for more.

Pitfalls and Troubleshooting

  1. Ghost properties: renamed topics leave old retained values behind. Clear them explicitly.
  2. Retained commands: never retain command topics; devices will re-execute them on reconnect.
  3. Stale values shown as live: include timestamps and honour the status topic.
  4. Oversized retained payloads: keep each value small; the public broker accepts up to 64 KB per message and the free Serverless plan 256 KB.
  5. No atomic snapshot: if several properties must change together, publish them as one retained JSON document instead.
  6. Broker restarts: plan for retained state to be republished by devices on reconnect rather than assuming it survives broker restarts forever. Devices publishing full state on connect makes the twin self-healing.

When Retained Topics Are Not Enough

Retained topics are ideal for current state. Add other components when you need history (a time-series subscriber), queries across assets or per-twin access control (a twin service such as Eclipse Ditto with MQTT), or desired vs reported handling at scale. For the desired/reported pattern on retained topics, read digital twin state synchronization.

Run Your Retained-State Twin on ConnectMQ

For real assets, move from the public broker to a private broker on ConnectMQ, a managed MQTT broker with MQTT 3.1.1 and 5.0, retained messages, message expiry and TLS. Replace the host with your broker, add YOUR_USERNAME, YOUR_PASSWORD and YOUR_CLIENT_ID from the portal, and switch to port 8883 or 8084 for browsers. Sign up at app.connectmq.com on the free Serverless plan, no credit card required, and watch your twin's retained topics in the live topic explorer.

Frequently Asked Questions

What is a retained message in MQTT?

It is a message the broker stores as the last known value for its topic and delivers immediately to any new subscriber of that topic.

How do I delete a retained MQTT message?

Publish a zero-length payload with the retain flag to the same topic, for example mosquitto_pub -t <topic> -r -n.

Should each twin property have its own retained topic?

It works well for independent properties and small updates. Use one retained JSON document per asset when several values must change together atomically.

Can retained messages replace a database for a digital twin?

For current state, often yes. For history, cross-asset queries or audit trails, add a database subscriber alongside the broker.

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.