Moving values from an OPC UA server to an MQTT broker is easy. A few lines of code read a node and publish the number. Moving the meaning of those values is the hard part, and it is where most bridges fail quietly: the numbers arrive, but nobody downstream can tell what unit they are in, whether the reading was valid, when it was taken, or which machine it belongs to.
This guide is about that second problem. It covers what context an OPC UA server actually holds, what MQTT gives you to carry it, the mapping decisions in between, and the tests that show whether anything was lost. It is vendor-neutral; where Proxus is mentioned, it is as one concrete example of the choices described. For the broader protocol comparison, see MQTT vs OPC UA in modern IIoT.
Why bridge at all
OPC UA and MQTT solve different problems. OPC UA is a rich client/server protocol: a client connects to one server, browses its address space, reads and writes values, calls methods and subscribes to changes. It is excellent for talking to one machine or one SCADA server. It is less suited to fan-out, where ten systems in three networks all want the same value, because each of them would need its own session, its own certificate trust and its own route into the plant network.
MQTT is a publish/subscribe transport. A broker sits in the middle; producers publish to topics and consumers subscribe. Adding a consumer does not add load to the machine. That is why plants bridge: OPC UA on the plant side, where its structure and security fit, and MQTT towards everything else, often as the transport of a Unified Namespace.
The bridge is where context is either preserved or thrown away.
What context OPC UA carries
Before mapping anything, list what the source gives you. For each variable node, a typical OPC UA server exposes:
| Item | Example | Why it matters downstream |
|---|---|---|
| NodeId | ns=3;s=Line1.Filler.Speed | The stable address. Changes only when the PLC program is restructured. |
| BrowseName and DisplayName | Speed, Filler speed | Human-readable names; often the basis for topic names. |
| Position in the address space | Objects/Line1/Filler/Speed | The server's own hierarchy, which may or may not match your plant model. |
| DataType | Double, Int16, Boolean, a structure | Determines how the value should be encoded and interpreted. |
| Value | 82.4 | The number itself. |
| StatusCode | Good, Uncertain_SensorNotAccurate, Bad_CommunicationError | Whether the value can be trusted. |
| SourceTimestamp | 2026-10-16T08:14:22.395Z | When the source (PLC or sensor) produced the value, if the server provides it. |
| ServerTimestamp | 2026-10-16T08:14:22.410Z | When the server received or last confirmed it. |
| EngineeringUnits and EURange | rpm, 0..120 | Unit and expected range, if the server models them as properties. |
| Type definitions | A FillerType object with defined members | The information model: what kind of thing this is and what it contains. |
Not every server fills every item. A PLC's built-in OPC UA server may expose only names, types and values; a well-modelled server built on a companion specification may expose units, ranges and object types. The first job is to find out which of these your servers actually provide, by browsing them with a generic client and noting what is there.
What MQTT gives you to carry it
MQTT itself carries very little: a topic (a string), a payload (bytes), a QoS level, and a retain flag. MQTT 5 adds user properties and content type. Everything else has to be designed:
- The topic can carry location and identity.
- The payload has to carry value, time, quality, type and unit.
- Retained metadata topics can carry things that rarely change, such as units, ranges and descriptions.
- Sparkplug B, if you use it, defines a structure for much of this: typed metrics, birth messages that describe every metric, and properties for quality and unit.
The bridge's job is to place each OPC UA item into one of these.
Decide how to map the hierarchy
There are two ways to turn an OPC UA address space into topics, and the choice affects everything after it.
Mirror the server
Publish each node under a topic that copies its browse path:
opcua/plc-line1/Objects/PLC_1/DataBlocks/DB_Filler/SpeedThis is quick, needs no modelling and keeps a one-to-one link to the source. It also exposes the PLC's internal structure to every consumer. When the PLC programmer reorganizes data blocks, topics change and consumers break. Two lines with the same machine but different PLC programs end up with different topics for the same thing.
Map to a plant model
Publish each value under its place in the plant, independent of how the server organizes it:
v1/Acme/Izmir/Packaging/Line1/Filler/telemetry/SpeedThis takes more work up front, because someone has to decide where each value belongs. In return, consumers see a stable structure that matches the plant, the same machine type looks the same on every line, and a PLC change is absorbed in the bridge configuration instead of in every consumer.
Use the plant model for anything that more than one consumer will use. Mirroring is fine for a diagnostic view or a short-lived test. Keep the NodeId as metadata on the tag so maintenance engineers can still find the source.
In Proxus, each OPC UA server (read by the OPC UA driver) becomes a device with a place in the plant (enterprise, site, area, process cell, equipment module), and the nodes you pick become its tags; the tag's address is the NodeId. The topic is then built from the place, the device name and the tag name, not from the server's browse path. The OPC UA guide walks through browsing and picking nodes.
Design the payload
A message on a telemetry topic should be self-explanatory. A minimal payload that preserves the important OPC UA context:
{
"ts": "2026-10-16T08:14:22.395Z",
"value": 82.4,
"dataType": "Double",
"unit": "rpm",
"quality": "Good"
}The following sections cover the fields that cause the most trouble.
Timestamps: say which one
There are at least three candidate times for a value read from OPC UA: the SourceTimestamp, the ServerTimestamp and the time the bridge received it. On a polled connection the gap can be the whole polling interval; on a busy server it can be more. For trends at one-second resolution it rarely matters. For sequence-of-events analysis ("did the pressure drop before or after the valve closed?") it decides the answer.
Pick one, document it, and name it clearly. If you can, carry the source time when the server provides a meaningful one. If your bridge uses its own read time, say so, because consumers will otherwise assume it is the source time. Proxus, for instance, stamps each value with the time the gateway read it, which is consistent across protocols but is not the OPC UA SourceTimestamp.
All times should be UTC with an explicit Z or offset. Local time without an offset is a common source of one-hour errors twice a year.
Quality: do not launder bad values
OPC UA StatusCodes have three severities (Good, Uncertain, Bad) and many specific sub-codes. MQTT has nothing equivalent unless you add it. The worst outcome is a bridge that drops the status and publishes the value as if it were fine. A Bad_SensorFailure reading of 0.0 then looks exactly like a real zero.
Two defensible approaches:
- Carry the status. Map it to at least
Good,UncertainandBadin the payload, and optionally keep the original code for diagnostics. Consumers decide what to do with uncertain values. - Publish only good values, and expose failures elsewhere. Bad reads are not published as values; a state topic or a device status shows that reading failed. Consumers never see a bad value, but they must check status to tell "unchanged" from "unknown".
Proxus takes the second approach on its UNS telemetry topics: values that could not be read are not published there, and read errors show on the device page and in the device's status. Whichever approach you choose, never repeat the last good value with a new timestamp to fill a gap.
Data types: watch the edges
OPC UA has a richer type system than JSON. Things to decide explicitly:
- Integer widths.
UInt64values above 2^53 lose precision as JSON numbers in many languages. Send them as strings if they can be that large, or confirm that they cannot. - Booleans stored as integers. Some PLCs expose a status bit as
Int16. Map it to a boolean in the bridge if that is what it means, so consumers do not have to know. - Arrays and structures. A structure (an OPC UA ExtensionObject) or an array can be published as one JSON object, split into one topic per member, or flattened to a string. Splitting is the most consumer-friendly; one object keeps members consistent with each other. Decide per case.
- DateTime values. Publish them as ISO 8601 strings in UTC.
State the type in the payload. In Proxus, OPC UA types map to Proxus types on the tag (Boolean to Bool, Int32 to Int, Double to Double and so on, as listed in the OPC UA guide); arrays and types without a direct equivalent are read as text. The UNS payload then states the type it carries (Bool, Int, Long, Float, Double, DateTime or String).
Units and ranges
If the server models EngineeringUnits, carry them across. If it does not, which is common with PLC servers, add the unit in the bridge configuration when you create the tag. A value without a unit is a value someone will eventually misread. Ranges (EURange) are useful for validation and for scaling gauges; publish them in a retained metadata topic if consumers need them.
Context that is not in the server
Some context never existed in the OPC UA server: the order being produced, the product, the shift. Do not try to force it into every value's payload. Publish it as its own retained topics on the line or machine node, from the system that owns it (MES, ERP or an operator entry), and let consumers join it with the telemetry by time and place.
Reading the server: polling or subscriptions
OPC UA offers two ways to get values: read them on a schedule, or create a subscription with monitored items and let the server push changes.
Subscriptions are efficient for large numbers of slowly changing values. The server samples at the sampling interval and sends notifications at the publishing interval, optionally only when a value changes by more than a deadband. Subscriptions have settings that need care: queue size, sampling interval, publishing interval, keep-alive and lifetime counts. A misconfigured subscription silently drops intermediate values or stalls.
Polling (reading all tags together on a fixed interval) is simpler and fully predictable: one read per interval, with a known load on the server. It may read values that have not changed, and it can miss very short pulses between reads.
Neither is universally better. Choose subscriptions when you need to catch short changes and the server handles subscriptions well; choose polling when you value predictability or the server's subscription support is weak. Proxus reads OPC UA devices by polling: the gateway reads all tags of the device together, once per Read every interval, and does not use subscriptions. Set the interval to how fresh the values must be, and use a PLC-side latch or counter for events shorter than that.
Reporting by exception downstream
Whether you poll or subscribe on the OPC UA side, you can still publish to MQTT only on change, to reduce traffic for consumers that do not need every read. Be careful with deadbands on the MQTT side: a value that changed by less than the deadband is never published, so a consumer cannot distinguish "steady" from "stuck". A maximum reporting interval (publish at least every N seconds even without change) solves this. In Proxus, the per-device UNS topics carry every read, while each outbound target has a transport strategy: every value, only changed values, or only what a pipeline sends to it.
Security on both sides
A bridge connects two security domains, and each side has its own rules.
The OPC UA side
- Use Sign or Sign & encrypt with a current security policy. None lets anyone on the network read and change values; keep it for test benches.
- Trust has to be set up in both directions: the bridge must trust the server's certificate, and the server must trust the bridge's certificate. Compare fingerprints before you trust, rather than accepting whatever is offered on first connection.
- Keep trust decisions in one place. If ten bridges each keep their own trust list, revoking a compromised certificate becomes a hunt. In Proxus, the Certificate Store on the hub is the single place where OPC UA server certificates are trusted or rejected, and every gateway follows it within a minute.
- Prefer a dedicated OPC UA user with read-only rights on the server for a bridge that only reads.
The MQTT side
- Use TLS (port 8883 by convention) and verify the broker against a CA you control.
- Give each consumer its own credential where the broker supports it.
- Know what your broker can restrict per topic. Some brokers have per-topic ACLs, some do not. If yours does not, restrict who gets credentials at all, and enforce finer visibility in the platform that serves people and applications.
- Do not turn the bridge into an unaudited write path. Reading from OPC UA and publishing to MQTT is one direction. If a consumer must write back to the machine, that is a separate path with its own authorization, validation and audit.
Sparkplug B or plain JSON
Sparkplug B is a specification on top of MQTT that standardizes topics, payload encoding (Protocol Buffers), and session state. A Sparkplug edge node publishes birth messages that describe every metric it will send, with types and properties, then data messages with values, and a death message when it disconnects.
Sparkplug B is a good fit when several vendors' applications must understand each other without a custom contract, or when bandwidth matters. It brings its own rules: sequence numbers, rebirth requests, and host applications that track state. Plain JSON with a documented schema is easier to read and debug, and is enough when you control both ends.
Some bridges let you choose per broker. Proxus publishes JSON by default; set the gateway's UNS payload format to Sparkplug B and it publishes Sparkplug B 3.0 instead, with each device as a Sparkplug device and each metric carrying source, quality and unit properties. Sparkplug B can also be used as an outbound target to another system. Proxus does not consume Sparkplug B from other nodes; it is publish only.
When the link drops
A bridge sits between a plant network and a wider network, and the wider link will fail sometimes. Decide in advance:
- Where values wait. A bridge without a buffer loses everything read during an outage. A bridge with a buffer can deliver it later, with the original timestamps.
- How long they wait. A buffer is bounded by disk and by policy. Size it for the longest outage you need to survive.
- What consumers see on replay. Buffered values arrive late and in a burst. Consumers that assume "now" for every message will misplace them; consumers should use the payload timestamp.
- Duplicates. Most buffered delivery is at-least-once. After a reconnect, a consumer may see a message twice. Make writes idempotent, for example keyed on device, tag and timestamp.
In Proxus, values travel from each gateway to the hub through a buffered message server: 48 hours by default and up to 30 days per gateway, replayed with the original read times. Outbound targets, including MQTT brokers, queue at the gateway with the same retention and are written in order, at least once, when the target is back. The edge historian guide covers buffering and backfill in detail.
Test that context survived
Before declaring the bridge done, check each kind of context end to end:
- Identity. Pick five values. From the MQTT topic alone, can someone who knows the plant say which machine and which measurement each one is?
- Units. Does every telemetry payload, or its metadata topic, carry a unit? Compare a few against the HMI.
- Time. Change a value on the PLC at a known moment and compare the timestamp in the MQTT payload. Is the difference what you documented?
- Quality. Disconnect a sensor or stop the OPC UA server. What do consumers see: a bad quality, no messages and an offline status, or the old value with new timestamps? Only the last one is wrong.
- Types. Check a boolean, an integer, a large counter and a string. Do they arrive as the right JSON types?
- Outage. Cut the network between the bridge and the broker for longer than a polling interval. Do the values from the gap arrive afterwards, with their original times?
Checklist
- You have browsed every source server and recorded which context it actually provides.
- Topics follow the plant model, not the server's browse path, and the NodeId is kept as tag metadata.
- Every payload carries time, value, type, unit and quality, and the meaning of the time is documented.
- Bad reads never appear as good values.
- Large integers, booleans-as-integers, arrays and structures each have a written mapping rule.
- OPC UA connections use Sign or Sign & encrypt, with trust managed in one place.
- The MQTT side uses TLS and per-consumer credentials, and you know its per-topic limits.
- The bridge buffers through outages, and consumers handle late and duplicate messages.
Further reading
- MQTT vs OPC UA in modern IIoT: the protocol trade-offs in more depth.
- Unified Namespace architecture: a practical guide: naming, payload and ownership rules for the namespace the bridge feeds.
- Store and forward in IIoT: the pattern behind buffered delivery.
- Already running an OPC server such as Kepware? Proxus compared with Kepware explains where a connectivity server ends and a data layer begins, and how the two work together.
- In the Proxus documentation: Connect an OPC UA server, MQTT and the UNS topics, Certificates and the protocol reference.