Industrial IoT

OPC UA PubSub over MQTT Explained: Messages, Encodings and Topics

OPC UA Part 14 lets OPC UA publishers send data through any standard MQTT broker. Learn the PubSub building blocks, JSON vs UADP encodings, topic and metadata conventions, and how to publish and decode messages yourself.

Diagram of OPC UA PubSub over MQTT with publishers sending NetworkMessages through a broker to multiple subscribers
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

OPC UA PubSub over MQTT combines OPC UA's information model with MQTT's broker-based distribution, so an OPC UA device can publish data once and any number of subscribers can receive it through a standard broker. In this explainer you will learn how Part 14 of the OPC UA specification structures PubSub, the difference between JSON and UADP encodings, how topics and metadata are organised, and how these messages flow through an ordinary MQTT broker. You will also publish and decode a JSON NetworkMessage yourself.

Why OPC UA Added PubSub

Classic OPC UA is client/server: a client opens a session, creates subscriptions and receives notifications over that session. It is powerful, but it scales poorly when many consumers want the same data and it requires clients to reach servers directly, which is awkward across firewalls and into the cloud. Our OPC UA vs MQTT comparison covers that tension in detail.

Part 14 of the specification, introduced with OPC UA 1.04 and extended in 1.05, adds a publish/subscribe model. Publishers send messages without knowing who receives them; subscribers filter what they need. Part 14 defines two families of transport:

  • Broker-less, such as UDP multicast or Ethernet (including TSN), for low-latency, deterministic communication between controllers on the same network.
  • Broker-based, using MQTT or AMQP, for distributing data to many consumers, across networks and into the cloud.

This article is about the second family with MQTT as the transport.

The PubSub Building Blocks

Part 14 introduces a vocabulary that is worth learning before looking at messages.

Concept What it is
PublishedDataSet A named collection of variables or events to publish, such as all spindle values of a CNC
DataSetWriter Turns a PublishedDataSet into DataSetMessages, with a numeric DataSetWriterId
WriterGroup Groups DataSetWriters that share settings such as publishing interval and encoding; produces NetworkMessages
PublisherId Identifies the publishing application, as a number or string
NetworkMessage The unit that travels over the transport: a header plus one or more DataSetMessages
DataSetMessage The values of one dataset at one moment, either a full key frame or a delta frame
DataSetMetaData Describes the fields of a dataset: names, data types, versions
DataSetReader On the subscriber side, filters and decodes messages for one dataset

The important idea is that the payload is not arbitrary JSON invented by each vendor. It is a defined message structure, with sequence numbers, metadata versions and a way to describe fields, so a generic subscriber can interpret data from any compliant publisher.

JSON vs UADP Encoding

Part 14 defines two message encodings, and both can be carried over MQTT.

Aspect JSON encoding UADP (binary) encoding
Format Human-readable JSON Compact binary, OPC UA binary types
Size Larger Much smaller
Readability Readable in any MQTT client or log Needs an OPC UA PubSub decoder
Typical consumers Cloud services, IT tools, dashboards, scripts OPC UA stacks, controllers, bandwidth-constrained links
Field names Can be included in the message Usually require metadata to interpret
Best for Integration and interoperability with IT Efficiency and determinism

For cloud integration, JSON is the common choice because every language can parse it and you can inspect messages directly in a topic explorer. UADP makes sense when bandwidth matters or when both ends are OPC UA stacks.

Anatomy of a JSON NetworkMessage

A JSON-encoded data message looks like this (field set simplified for readability):

{
  "MessageId": "8f1c2e7a-0b44-4c55-9a1e-3f0d9d2b6c11",
  "MessageType": "ua-data",
  "PublisherId": "cnc-cell-07",
  "Messages": [
    {
      "DataSetWriterId": 101,
      "SequenceNumber": 4521,
      "MetaDataVersion": { "MajorVersion": 1, "MinorVersion": 3 },
      "Timestamp": "2026-10-16T09:30:12.512Z",
      "MessageType": "ua-keyframe",
      "Payload": {
        "SpindleTemp": 47.3,
        "SpindleSpeed": 12000,
        "PartCount": 1843
      }
    }
  ]
}

Key fields to understand:

  • MessageType at the top level distinguishes data (ua-data) from metadata (ua-metadata) and, in newer versions, status and other discovery messages.
  • PublisherId and DataSetWriterId together identify exactly which dataset this is.
  • SequenceNumber lets subscribers detect gaps and duplicates.
  • MetaDataVersion tells the subscriber which version of the field description applies. If it changes, the subscriber should fetch new metadata before trusting field positions or types.
  • Key frames vs delta frames: a key frame carries every field; a delta frame carries only the fields that changed, which reduces traffic for slowly changing data.

Payload values can be encoded in more compact or more verbose forms (the spec distinguishes reversible and non-reversible encodings, and 1.05 refines this). A compact form gives plain values as above; a verbose form wraps each value with its type, status and timestamps so it can be decoded back into an exact OPC UA Variant.

Topics and Metadata on the Broker

In OPC UA 1.04, the MQTT topic (called the queue name) is simply configured on each WriterGroup or DataSetWriter. OPC UA 1.05 adds a recommended default topic tree so subscribers can discover publishers in a predictable way. It follows this shape:

<prefix>/<encoding>/<message type>/<PublisherId>/<WriterGroup>/<DataSetWriter>

opcua/json/data/cnc-cell-07/fast/spindle
opcua/json/metadata/cnc-cell-07/fast/spindle
opcua/json/status/cnc-cell-07

The prefix defaults to opcua but is configurable, which matters when you share a broker with other systems. Three conventions are worth following:

  1. Metadata messages are published as retained, so a subscriber that connects later receives the field description immediately.
  2. Status messages are retained and paired with a Last Will, so subscribers know whether a publisher is online.
  3. QoS is configurable per writer group, mapping to MQTT QoS 0, 1 or 2 (best effort, at least once, exactly once).

None of this needs special broker support. Retained messages, Last Will, wildcards and QoS are standard MQTT features, which is exactly why Part 14 chose MQTT as a transport. For a refresher on delivery guarantees, see MQTT QoS levels explained.

How Messages Flow Through a Standard Broker

From the broker's point of view, an OPC UA PubSub publisher is just another MQTT client. It connects, optionally authenticates, sets a Last Will and publishes payloads to topics. The broker does not parse the OPC UA structure; it routes bytes based on topics and subscriptions. That means any standards-compliant MQTT broker can carry OPC UA PubSub traffic, including a hosted MQTT broker such as ConnectMQ.

A subscriber typically:

  1. Subscribes to the metadata topic, receives the retained metadata and builds a field map.
  2. Subscribes to the data topic or a wildcard such as opcua/json/data/#.
  3. Checks MetaDataVersion against its cached metadata and SequenceNumber for gaps.
  4. Applies delta frames to its last known key frame to maintain current state.

Hands-On: Publish and Decode a JSON NetworkMessage

The public broker limits topics to eight levels, so the example replaces the opcua prefix with a unique prefix of its own. Install paho-mqtt 2.x first with pip install "paho-mqtt>=2.0".

The publisher sends a retained metadata message and then a key frame every two seconds:

# pubsub_publisher.py - emits OPC UA Part 14 style JSON messages over MQTT
import json, random, time, uuid
from datetime import datetime, timezone
import paho.mqtt.client as mqtt

PREFIX = "connectmq/demo/c3d9/json"          # stands in for the default "opcua/json"
PUBLISHER_ID = "cnc-cell-07"
DATA_TOPIC = f"{PREFIX}/data/{PUBLISHER_ID}/fast/spindle"
META_TOPIC = f"{PREFIX}/metadata/{PUBLISHER_ID}/fast/spindle"

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"uapub-{uuid.uuid4().hex[:8]}")
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_start()

metadata = {
    "MessageId": str(uuid.uuid4()), "MessageType": "ua-metadata",
    "PublisherId": PUBLISHER_ID, "DataSetWriterId": 101,
    "MetaData": {
        "Name": "Spindle",
        "Fields": [
            {"Name": "SpindleTemp", "BuiltInType": 11},   # Double
            {"Name": "SpindleSpeed", "BuiltInType": 11},
            {"Name": "PartCount", "BuiltInType": 7}       # UInt32
        ],
        "ConfigurationVersion": {"MajorVersion": 1, "MinorVersion": 3}
    }
}
client.publish(META_TOPIC, json.dumps(metadata), qos=1, retain=True)

seq, parts = 0, 1840
while True:
    seq += 1
    parts += random.choice([0, 0, 1])
    msg = {
        "MessageId": str(uuid.uuid4()), "MessageType": "ua-data", "PublisherId": PUBLISHER_ID,
        "Messages": [{
            "DataSetWriterId": 101, "SequenceNumber": seq,
            "MetaDataVersion": {"MajorVersion": 1, "MinorVersion": 3},
            "Timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
            "MessageType": "ua-keyframe",
            "Payload": {"SpindleTemp": round(random.uniform(44, 50), 2),
                        "SpindleSpeed": random.choice([0.0, 8000.0, 12000.0]),
                        "PartCount": parts}
        }]
    }
    client.publish(DATA_TOPIC, json.dumps(msg), qos=1)
    time.sleep(2)

The subscriber caches metadata, checks versions and detects sequence gaps:

# pubsub_subscriber.py - decodes OPC UA JSON NetworkMessages from MQTT
import json, uuid
import paho.mqtt.client as mqtt

PREFIX = "connectmq/demo/c3d9/json"
meta_versions, last_seq = {}, {}

def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe(f"{PREFIX}/metadata/#", qos=1)
    client.subscribe(f"{PREFIX}/data/#", qos=1)

def on_message(client, userdata, message):
    nm = json.loads(message.payload)
    if nm["MessageType"] == "ua-metadata":
        key = (nm["PublisherId"], nm["DataSetWriterId"])
        meta_versions[key] = nm["MetaData"]["ConfigurationVersion"]
        print("metadata", key, [f["Name"] for f in nm["MetaData"]["Fields"]])
        return
    for dsm in nm.get("Messages", []):
        key = (nm["PublisherId"], dsm["DataSetWriterId"])
        if meta_versions.get(key) != dsm["MetaDataVersion"]:
            print("warning: metadata missing or outdated for", key)
        prev = last_seq.get(key)
        if prev is not None and dsm["SequenceNumber"] != prev + 1:
            print(f"gap detected: {prev} -> {dsm['SequenceNumber']}")
        last_seq[key] = dsm["SequenceNumber"]
        print(dsm["Timestamp"], dsm["Payload"])

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"uasub-{uuid.uuid4().hex[:8]}")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_forever()

Run the publisher, then start the subscriber at any time. Because the metadata is retained, the subscriber prints the field list first, then the data. Replace c3d9 with your own random string.

Practical Considerations

  • Mixing with a Unified Namespace. OPC UA PubSub topics are organised by publisher and writer group, not by plant hierarchy. Many teams keep the PubSub tree as is and run a small service that republishes selected fields into an ISA-95 style namespace; see our guide to ISA-95 MQTT topics.
  • Message size. Key frames with many fields can grow large. Check the broker's message size limit for your plan and prefer delta frames or several writer groups for wide datasets.
  • Security. Over MQTT you secure the transport with TLS and broker credentials. Part 14 also defines message-level signing and encryption with a key service, mainly used with UADP.
  • Tooling. Open-source stacks such as open62541 include PubSub with MQTT support, and several commercial PLCs and gateways can publish Part 14 JSON directly.

For the broader context of where this fits in a plant architecture, read our industrial IoT MQTT guide.

Get Started with ConnectMQ

Point your OPC UA PubSub publisher at a broker you do not have to operate. Create one for free at app.connectmq.com on the Serverless plan with no credit card, use the generated username, password and client ID with TLS on port 8883, and inspect your NetworkMessages in the portal's live topic explorer. ConnectMQ supports MQTT 3.1.1 and 5.0, retained messages and Last Will, everything Part 14 relies on.

Frequently Asked Questions

Does the MQTT broker need to understand OPC UA for PubSub to work?

No. The broker routes messages by topic and treats payloads as bytes. Any standards-compliant MQTT broker can carry OPC UA PubSub traffic.

Should I use JSON or UADP encoding for OPC UA PubSub over MQTT?

Use JSON when IT or cloud systems consume the data, because it is readable and easy to parse. Use UADP when bandwidth is tight or both ends are OPC UA stacks.

What is the difference between a key frame and a delta frame?

A key frame contains every field of the dataset; a delta frame contains only fields that changed. Subscribers apply delta frames to the last key frame to keep the full state.

Why should metadata messages be retained?

So a subscriber that connects after the publisher started still receives the field description immediately, without waiting for the publisher to resend it.

Related articles

IIoT Edge Gateway: What It Does and How to Choose One

An IIoT edge gateway connects machines that speak Modbus, OPC UA or proprietary protocols to MQTT and the cloud. Learn its core roles, common architectures and how to choose the right one.

7 min read

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.