Node-RED

Node-RED Modbus to MQTT: Poll Devices and Publish Values

Build a Node-RED Modbus gateway: poll holding registers and coils with node-red-contrib-modbus, decode scaling and 32-bit floats, publish JSON to MQTT by exception, and accept safe write commands.

Node-RED Modbus flow polling holding registers from a pump controller and publishing JSON values to MQTT
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

Node-RED Modbus flows are one of the quickest ways to get data out of meters, drives and PLCs that only speak Modbus and into MQTT, where dashboards, historians and cloud tools can use it. In this tutorial you will poll holding registers and coils with node-red-contrib-modbus, decode scaled integers and 32-bit floats correctly, and publish clean JSON to a broker only when values change. You will also see how to handle write commands safely and how to test everything without real hardware.

Why Bridge Modbus to MQTT with Node-RED

Modbus is a request/response protocol from 1979: a client (master) asks a server (slave) for a block of registers and gets raw 16-bit numbers back. It has no units, no names, no timestamps and no push. Every consumer that wants the data has to poll the device itself, and many devices handle only a few simultaneous connections.

MQTT turns that around. One gateway polls the device, gives the numbers meaning, and publishes them once to a broker. Any number of subscribers then receive named, scaled values the moment they change. Node-RED is a natural gateway because polling, decoding, filtering and publishing are each one node, and the logic stays visible to the maintenance team. For the bigger architectural picture, the guide to Modbus TCP to MQTT compares Node-RED with other gateway options.

Install node-red-contrib-modbus

Install the package from Manage palette → Install or from the command line:

cd ~/.node-red
npm install node-red-contrib-modbus
# restart Node-RED afterwards

It provides these nodes, among others:

Node Purpose
Modbus Read Polls a fixed block of registers on a timer
Modbus Getter Reads on demand when a message arrives
Modbus Flex Getter Reads whatever the incoming message specifies
Modbus Write Writes coils or registers
Modbus Server A simple Modbus TCP server for testing
Modbus Client (config) TCP or serial connection shared by the nodes

Understand Your Register Map First

Every Modbus project starts with the device's register map. For this tutorial, imagine a pump controller with this map:

Data Type Address (0-based) Classic notation Scaling
Temperature Holding register, signed 16-bit 0 40001 value / 10, °C
Pressure Holding register, unsigned 16-bit 1 40002 value / 100, bar
Flow Holding registers, 32-bit float, big-endian 2-3 40003-40004 none, m³/h
Pump running Coil 0 00001 boolean

The single most common Node-RED Modbus mistake is the address. Datasheets often use "40001" notation, which is one-based and includes a type prefix. The protocol address, and the one you enter in Node-RED, is zero-based without the prefix: 40001 becomes holding register address 0. If your values look shifted by one register, this is why.

Test Without Hardware: A Local Modbus Server

You can follow the whole tutorial without a device by running a Modbus server inside Node-RED.

  1. Drag a Modbus Server node onto a separate tab. Set it to listen on 0.0.0.0, port 10502 (unprivileged, so no root needed). Deploy.
  2. Create a Modbus Client config node: type TCP, host 127.0.0.1, port 10502, unit ID 1.
  3. Add an inject node repeating every 2 seconds, a function node that generates simulated register values, and a Modbus Write node using that client with function code 16 (preset multiple registers), address 0, quantity 4.

The simulator function:

// Function node "simulate pump registers" -> Modbus Write (FC16, address 0, quantity 4)
const temperature = 40 + Math.random() * 5;           // °C
const pressure = 3.2 + Math.random() * 0.3;           // bar
const flowRate = 12 + Math.random() * 2;              // m³/h

const buf = Buffer.alloc(4);
buf.writeFloatBE(flowRate, 0);

msg.payload = [
    Math.round(temperature * 10),      // register 0
    Math.round(pressure * 100),        // register 1
    buf.readUInt16BE(0),               // register 2: float high word
    buf.readUInt16BE(2)                // register 3: float low word
];
return msg;

Add a second write node on the coil: function code 5 (force single coil), address 0, fed with true from an inject node. Your fake pump controller is now running.

Build the Node-RED Modbus Polling Flow

Step 1: Poll the registers

Add a Modbus Read node with the same client:

  • FC: 3, Read Holding Registers
  • Address: 0, Quantity: 4
  • Poll rate: 2 seconds

Its first output emits msg.payload as an array of four raw numbers, for example [423, 334, 16708, 26214]. The second output carries the raw buffer and response details, which is useful for debugging byte order.

For the coil, add a second Modbus Read node with FC 1 (Read Coil Status), address 0, quantity 1, and a slower poll of 5 seconds.

Step 2: Decode raw registers into engineering values

// Function node "decode pump registers"
const r = msg.payload;
if (!Array.isArray(r) || r.length < 4) {
    node.warn("Unexpected register block");
    return null;
}

// Signed 16-bit conversion for temperature
const int16 = v => (v > 32767 ? v - 65536 : v);

// 32-bit float from two registers, big-endian word order
const buf = Buffer.alloc(4);
buf.writeUInt16BE(r[2], 0);
buf.writeUInt16BE(r[3], 2);
const flowRate = buf.readFloatBE(0);

const values = {
    temperature: Math.round(int16(r[0])) / 10,
    pressure: Math.round(r[1]) / 100,
    flow: Math.round(flowRate * 100) / 100
};

// Store for combining with the coil reading
flow.set("pump1_values", values);

msg.topic = "connectmq/demo/a7f3k2/plant1/utilities/pump1/telemetry";
msg.payload = {
    ...values,
    running: flow.get("pump1_running") === true
};
return msg;

Connect the coil read to a tiny function that stores the state: flow.set("pump1_running", msg.payload[0] === true); return null;. Many devices use "word-swapped" floats, where the low word comes first. If your flow value looks absurd, such as 1.2e-38 or -4.5e+12, swap r[2] and r[3] and try again.

Step 3: Publish only when something changes

Polling every 2 seconds and publishing every result wastes bandwidth and fills subscribers with duplicate data. Insert a filter node (formerly called RBE) set to "block unless value changes" on msg.payload, or use a deadband version on individual values if they jitter. The filter compares the whole object, which is why the decode function does not add a timestamp. After the filter, add a change node that sets msg.payload.ts to a timestamp. Reporting by exception is a core MQTT pattern, but a periodic heartbeat is still worth adding for slow-changing values so subscribers can tell a quiet device from a dead one.

Step 4: Publish to MQTT

Add an mqtt out node with an empty topic so msg.topic is used. For testing, configure the broker as broker.connectmq.com, port 1883, with a unique client ID, which is the public MQTT test broker. Replace a7f3k2 with your own string. Then watch the output:

mosquitto_sub -h broker.connectmq.com -p 1883 -i sub-modbus-a7f3k2 -v \
  -t 'connectmq/demo/a7f3k2/plant1/#'

You should see messages like {"temperature":42.3,"pressure":3.34,"flow":12.81,"running":true,"ts":...}. Do not send real plant data to the public broker: anyone can subscribe to it.

Writing to Modbus from MQTT, Safely

Reading is low risk; writing to a pump is not. If you accept commands from MQTT, put guardrails in the flow:

  1. Subscribe to a dedicated command topic, such as .../pump1/cmd, never a wildcard.
  2. Validate the payload against a whitelist of allowed actions and value ranges in a function node.
  3. Map each action to a fixed register or coil. Never let the message choose the address.
  4. Publish the result to a .../pump1/cmd/ack topic so the sender knows whether the write happened.
  5. Use a private broker with authentication for any topic that can change equipment state.
// Function node "validate pump command" -> Modbus Write (FC5, coil 0)
const cmd = String(msg.payload).trim().toUpperCase();
if (cmd !== "START" && cmd !== "STOP") {
    node.warn("Rejected command: " + cmd);
    return null;
}
msg.payload = cmd === "START";
return msg;

Modbus RTU over RS-485

For serial devices, change only the client config: type Serial, port such as /dev/ttyUSB0, baud rate, parity and stop bits matching the device (9600 8N1 is common), and the RTU serial type. Each device on the bus has its own unit ID. Keep poll rates conservative, because one RS-485 bus serves all devices in turn, and a timeout on one device delays the others. The Modbus RTU to MQTT guide covers wiring and termination.

Troubleshooting Node-RED Modbus Flows

Symptom Likely cause Fix
Timeout errors Wrong IP, port or unit ID; firewall Check with a desktop Modbus tool first
Illegal data address (exception 2) Off-by-one address or wrong function code Use zero-based addresses; holding vs input registers
Values shifted by one register 40001 notation entered literally Subtract the offset
Absurd float values Word order differs Swap the two registers
Negative numbers show as 65,000+ Signed value read as unsigned Apply the int16 conversion
Device stops responding under load Too many clients or too fast polling One gateway, slower polls, merge reads into blocks

From Gateway to Production

Once values flow, the gateway is only as useful as the broker it publishes to. On a site with several Node-RED gateways, a SCADA client and a historian, all of them should share one authenticated broker reachable from both the plant and the office network. A cloud MQTT broker is a simple way to achieve that, because every gateway connects outbound on port 8883 and no inbound ports open into the OT network. ConnectMQ also recognises ISA-95 style topics such as plant1/utilities/pump1/telemetry and shows devices by those levels in its portal. If you would rather not run broker infrastructure at all, a managed MQTT broker takes patching and monitoring off your plate.

Other industrial protocols follow the same pattern: see Node-RED OPC UA to MQTT for modern controllers and Node-RED with Siemens S7 for S7 PLCs. For broker setup in Node-RED, the Node-RED MQTT tutorial has the basics.

Connect Your Modbus Gateway to ConnectMQ

Sign up at app.connectmq.com on the free Serverless plan, no credit card required. Create a broker, paste the generated username, password and client ID into the Node-RED config node, enable TLS on port 8883, and your Modbus values are published privately and encrypted. Use the ConnectMQ live topic explorer to verify every register you decoded.

Frequently Asked Questions

Does node-red-contrib-modbus support both Modbus TCP and RTU?

Yes. The Modbus Client config node supports TCP and serial connections, including RTU over RS-485 adapters. The read and write nodes work the same way for both.

Why is my Modbus address off by one in Node-RED?

Datasheets often use one-based 40001 style numbering, while Node-RED uses zero-based protocol addresses. Holding register 40001 is address 0 in the Modbus Read node.

How do I read a 32-bit float from Modbus in Node-RED?

Read two consecutive registers, write them into a 4-byte buffer and call readFloatBE. If the result looks wrong, swap the two words because some devices use the opposite word order.

How often should Node-RED poll a Modbus device?

Poll as slowly as your use case allows, often 1 to 10 seconds for monitoring. Combine registers into blocks and publish by exception to keep both the device and the broker lightly loaded.

Related articles

Node-RED OPC UA Client to MQTT with ISA-95 Topics

Use node-red-contrib-opcua to subscribe to OPC UA variables on a PLC or simulation server, map node IDs to ISA-95 style MQTT topics, and publish values with quality and timestamps.

9 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.