OPC UA vs MQTT: Both, Not Either, in a Unified Namespace

Daniel Price9 min read
B&R AutomationIndustrial NetworkingTechnical Reference
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Follow the packet and the protocol argument resolves itself. A tag value leaves the PLC over an OPC UA session, lands in an Ignition tag provider, gets serialized into a Sparkplug B protobuf payload by MQTT Transmission, crosses one TCP connection to a broker, and is reassembled by MQTT Engine on whatever gateway subscribed. Two protocols, two different jobs, no overlap. The mistake is treating them as competing options for the same hop.

Where does a tag value actually travel?

Draw the hops before you configure anything. Each one has a different initiator, a different failure mode, and a different diagnostic.

Hop Protocol Who initiates Failure signature
PLC to Ignition gateway OPC UA (TCP, port 4840 by default) Ignition, as OPC UA client Device shows Disconnected; tag quality goes Bad_NotConnected
Gateway tag provider to transmitter Internal tag reference by path MQTT Transmission, scanning its configured tag path Tags exist but never appear in any topic
Transmitter to broker MQTT 3.1.1 / Sparkplug B (1883 plain, 8883 TLS) Transmission, one persistent TCP session No topics under spBv1.0/#; transmitter status not connected
Broker to remote gateway MQTT subscribe, Sparkplug B decode MQTT Engine Engine tag provider empty or all tags stale

Layer one first on the two hops that cross a wire. Link light, switch port, VLAN, then ICMP, then a TCP connect to the port. An OPC UA session that will not establish is almost never an OPC UA problem until you have proven the socket opens.

Why keep OPC UA on the PLC hop?

OPC UA is session-oriented and client-driven. Ignition browses the server address space, gets datatypes and quality codes for free, creates subscriptions with a sampling interval and optional deadband, and can write back on the same session. That is exactly what a controller-facing driver needs. A PLC that speaks Sparkplug natively is the exception, not the rule; putting MQTT on the controller hop usually means adding an edge device that does the OPC UA read anyway, one hop further out.

MQTT earns its place at the boundary. It is a single outbound TCP session to a broker, which survives NAT, firewalls that permit outbound only, and links that come and go. Report-by-exception keeps the bandwidth proportional to change, not to tag count. That is the wrong trade inside a panel and the right trade to a remote gateway or a cloud subscriber.

Check before moving on: in the gateway, every device connection reads Connected, and the tags you intend to publish read quality Good in the tag browser. Nothing downstream can be better than this.

How do you shape the namespace before you publish?

The namespace is built in the tag provider, not in the broker. Create a dedicated provider for the data you intend to publish — call it something explicit like OPCUA_MQTT — and give it a folder tree that reflects the physical plant. That tree becomes the topic and metric structure verbatim; whatever sloppiness exists in the folder names is permanent once subscribers bind to it.

Use an ISA-95 style hierarchy so the levels have meaning to someone who has never seen your gateway: enterprise, site, area, line, cell. For a three-PLC brewery that resolves cleanly to site, area (hot side, cold side, packaging), and one device per PLC. Keep tag names identical across identical equipment — Temp_PV on one vessel and TT101 on the next destroys every reusable template you would otherwise build downstream.

Reference the OPC tags into this provider rather than duplicating logic. The provider is a presentation layer over the driver tags, and it exists so the published structure can change without touching device configuration.

Check before moving on: browse OPCUA_MQTT and read the full path of a leaf tag aloud. If it does not describe the physical thing it measures, fix it now.

What does the transmitter turn your folders into?

Sparkplug B addresses every message with a fixed topic namespace:

spBv1.0/<group_id>/<message_type>/<edge_node_id>/<device_id>

message_type: NBIRTH | NDEATH | NDATA | NCMD
               DBIRTH | DDEATH | DDATA | DCMD | STATE

Those three IDs come from one of two places, and mixing the two approaches is where commissioning goes sideways.

Topic element Source if set in the transmitter Source if left blank Sensible mapping
group_id Transmitter setting, fixed for all its tags First folder level under the transmitter's tag path Site or area
edge_node_id Transmitter setting Next folder level The gateway or edge device doing the publishing
device_id Transmitter setting Next folder level One per PLC
Metric name Remaining folder path plus tag name Equipment plus signal

Setting group and node explicitly in the transmitter and letting folders supply the device level is the common production pattern: the node ID identifies the publishing gateway, which is a property of the infrastructure, while devices are a property of the plant. Driving all three from folders works and is more flexible, but it means a folder rename silently retires an edge node and creates a new one.

Edge node IDs must be unique across the entire broker. Two transmitters sharing a group_id and edge_node_id will fight: each connect publishes an NBIRTH, each disconnect fires the other's Will, and subscribers see an endless BIRTH/DEATH cycle with tags flapping between Good and Stale.

Check before moving on: subscribe to spBv1.0/# and confirm the topic strings that appear are the ones you drew. One NBIRTH per gateway, one DBIRTH per PLC, nothing unexpected.

Why does MQTT Explorer show unreadable payloads?

Because Sparkplug B payloads are Google Protocol Buffers, not JSON. A generic MQTT client renders the topic tree correctly and the payload as binary noise. That is confirmation the transmitter is working, not evidence of a fault.

To read values, use a client that decodes Sparkplug B: MQTT.fx has a Sparkplug B decode mode — look for v1.7.1 if you want the last free release — and Node-RED with a Sparkplug or protobuf decode node will render the metric array, alias, datatype, timestamp, and value. Open-source Sparkplug demo clients on GitHub cover the same job for scripted checks.

Symptom Likely cause Test
Topics present, payload unreadable Protobuf payload in a non-Sparkplug client Reopen with a Sparkplug-aware client
No topics at all under spBv1.0/# Transmitter not connected, broker ACL denies publish, or 1883/8883 blocked Transmitter status in the gateway; TCP connect to the broker port
New subscriber sees nothing until a value changes Sparkplug publishes with retain false; there is no retained last value Force a value change, or send a Rebirth command
Topics appear once, never update Report-by-exception with a static value or a deadband set too wide Change the source value in the PLC and watch for DDATA
Continuous NBIRTH/NDEATH cycling Duplicate group/edge node ID on the broker Audit every transmitter's ID pair
Engine tags stale after a network blip Sequence gap detected, rebirth pending Watch for an NCMD Rebirth and the following NBIRTH

Which tags should the local project bind to?

The local HMI binds to the OPC-backed provider, not to MQTT. Publishing a tag to a broker and reading it back into the same gateway adds a serialization round trip, a broker dependency, and a second copy of every value, and it means a broker outage blanks the screens of an operator standing next to a perfectly healthy PLC.

MQTT Engine belongs on the other gateway. Point it at the same broker, subscribe to the group and node you published, and it builds its own tag provider — a separate provider from anything local — that mirrors the structure the transmitter defined. Remote screens, historians, and analytics bind there.

Writes travel the same path in reverse: the subscriber writes to an Engine tag, Engine publishes a DCMD, the transmitter receives it and writes through OPC UA to the PLC. That path only works if the transmitter is configured to accept commands and the underlying OPC tag is writable. If a remote setpoint change appears to succeed and then reverts, the write was accepted into the Engine tag and never made it out as a DCMD — check the command settings on the transmitter first.

Check before moving on: local screens still update with the broker deliberately stopped. If they freeze, something is bound to the wrong provider.

How do BIRTH and DEATH prove the link is alive?

Sparkplug's state model is the reason it is worth using instead of raw MQTT topics. At CONNECT, the transmitter registers its NDEATH as the MQTT Last Will and Testament. Then it publishes NBIRTH containing every metric it will ever send, with datatype, timestamp, alias, and current value, followed by a DBIRTH per device. From that point on, only changes are transmitted, as NDATA/DDATA.

Two mechanisms keep subscribers honest. The broker publishes the stored Will if the TCP session drops or the keepalive expires, so subscribers learn about a dead edge node without polling anything. And every message carries a rolling sequence number, 0 through 255; a subscriber that sees a gap knows it missed data and issues a Rebirth command, which forces a fresh NBIRTH with a complete value set. No stale value survives a reconnect undetected.

Check before moving on: pull the uplink at the publishing gateway. Within roughly 1.5 times the keepalive interval, NDEATH appears on the broker and Engine tags go stale on the subscriber. Plug it back in and watch NBIRTH and DBIRTH land, with quality returning to Good.

What does the end-to-end check look like?

  1. Confirm every OPC UA device connection reads Connected and sample tags read Good quality in the gateway tag browser.
  2. Browse the OPCUA_MQTT provider and verify folder paths match the plant hierarchy you designed.
  3. Confirm the transmitter reports connected to the broker, on the intended host and port, with TLS if the link leaves the plant.
  4. Subscribe to spBv1.0/# with a Sparkplug-aware client and verify exactly the expected group, node, and device IDs — no duplicates, no leftovers from earlier test configurations.
  5. Change one PLC value and confirm a DDATA arrives on the correct topic, with the correct metric name, datatype, and a timestamp that tracks the change.
  6. On the subscribing gateway, confirm the MQTT Engine provider built the same structure and that the tag mirrors the change.
  7. Write a setpoint from the Engine side and confirm the value lands in the PLC and reads back through the OPC UA path.
  8. Stop the broker. Local screens must keep updating; Engine tags must go stale rather than hold a false value.
  9. Restart the broker and confirm NBIRTH, DBIRTH, and restored quality on the subscriber without touching either gateway.
  10. Kill and restart the transmitter and confirm the sequence resets cleanly with a full BIRTH rather than a partial value set.

FAQ

Why does MQTT Explorer show binary garbage instead of my tag values?

Sparkplug B encodes payloads as Google Protocol Buffers, and generic MQTT clients render that as binary. Use a Sparkplug-aware client such as MQTT.fx with Sparkplug B decode enabled (v1.7.1 is the last free release) or a Node-RED flow with a Sparkplug decode node to see the metric names, datatypes, and values.

Why does a new subscriber see no data until a value changes?

Sparkplug publishes with the retain flag false, so the broker holds no last-known value for a late subscriber. Either wait for the next report-by-exception update or send a Rebirth command to the edge node, which forces a fresh NBIRTH and DBIRTH carrying every current value.

Why does my edge node keep cycling between BIRTH and DEATH on the broker?

Two transmitters are sharing the same group ID and edge node ID. Each connection triggers the other's Last Will, producing an endless BIRTH/DEATH loop and tags flapping between Good and Stale. Give every transmitter a unique edge node ID across the entire broker.

Back to blog