OPC UA vs MQTT: how we pick a protocol for industrial IoT
Two protocols, two philosophies. When we go OPC UA, when we go MQTT-SN, and why they often end up coexisting on the same plant.
In every Industry 4.0 project we’ve shipped over the past five years — from Carpigiani to STOMMPY to IMA — there’s always the same initial fork in the road: OPC UA or MQTT?. The answer is never “one is better than the other” — it’s “depends on where the data lives and who needs to consume it”.
The two worlds in two lines
OPC UA was born to speak with the OT (Operational Technology) world: PLCs, drives, industrial controllers. It’s session-based, strongly typed, with an information model that describes not just values but their semantics.
MQTT is from the IT world: telemetry, lightweight publish/subscribe, arbitrary payloads. It plays well with cloud, scalable brokers, microservices.
When we go OPC UA
When we need to:
- Read or write variables inside a PLC — Siemens, Beckhoff, Rockwell. Most of them today expose a native OPC UA server.
- Preserve the semantic mapping of the data: a temperature isn’t “a number”, it’s a variable with a unit of measure, a valid range, related alarms.
- Subscribe to variables with a dead-band: the server sends an update only when the value leaves a tolerance band, cutting traffic without polling.
import { OPCUAClient, AttributeIds } from "node-opcua";
const client = OPCUAClient.create({ endpointMustExist: false });
await client.connect("opc.tcp://192.168.10.50:4840");
const session = await client.createSession();
const dataValue = await session.read({
nodeId: "ns=4;s=|var|PLC.LineSpeed",
attributeId: AttributeIds.Value,
});
console.log("Line speed:", dataValue.value.value, "m/min");
When we go MQTT (and MQTT-SN)
When the data must:
- Leave the field and end up in cloud (AWS IoT Core, Azure IoT Hub, HiveMQ).
- Be consumed by many independent subscribers: dashboards, alerting systems, ML pipelines.
- Travel over unstable or constrained links: in those cases MQTT-SN over LoRa or cellular is almost a must.
import mqtt from "mqtt";
const client = mqtt.connect("mqtts://iot.fancypixel.cloud:8883", {
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
});
client.on("connect", () => {
client.subscribe("plant/+/line/+/speed");
});
client.on("message", (topic, payload) => {
const speed = JSON.parse(payload.toString());
console.log(`[${topic}]`, speed.value, speed.unit);
});
The pattern we use (almost) every time
In nine projects out of ten we end up using both:
- On the field, a gateway speaks OPC UA to PLCs.
- The gateway maps variables to MQTT topics under a known convention.
- Towards the cloud it speaks MQTT, with TLS and per-device auth.
The gateway is the “membrane” between OT and IT. That’s where we apply throttling, unit normalization, optional enrichment with contextual data (shift, batch, operator).
The worst risk in these projects isn’t picking the wrong protocol. It’s not designing the membrane: exposing sensitive PLCs straight to an internet-reachable broker. We’ve seen it happen, and it doesn’t end well.
Recommended reading
A small set of resources we keep close:
- OPC UA Part 1 — Overview and Concepts (free PDF from the OPC Foundation).
- MQTT Version 5.0 (OASIS standard).
- Sparkplug B: the Eclipse Foundation spec that brings industrial semantics on top of MQTT — worth knowing even if you don’t adopt it.
In our next article we’ll dig into Sparkplug B and show a real “store and forward” example for lines with intermittent connectivity.