Modbus Protocol Deep Dive: RTU, TCP, and the Industrial Workhorse Explained

Modbus Protocol Deep Dive: RTU, TCP, and the Industrial Workhorse Explained

Modbus Protocol Deep Dive: RTU, TCP, and the Industrial Workhorse Explained

Last Updated: October 2026

A Modbus request for ten holding registers is eight bytes on a serial wire. The reply is twenty-five. Nothing in either frame says what those ten registers mean, what unit they are in, when they were sampled, or whether the sensor behind them is healthy. That absence is both the reason the Modbus protocol has survived more than four decades and the reason it causes so many quiet, expensive integration bugs.

It matters in 2026 because the new layer of the plant, the digital twin, the unified namespace, the cloud historian, still has to pull data out of drives, meters, chillers and PLCs that speak Modbus and nothing else. The protocol itself is easy. What bites teams is the surrounding discipline: timing on RS-485, register maps that disagree on whether addressing starts at zero, 32-bit values split across two registers in an order nobody documented, and a TCP variant with no authentication at all.

You will leave with a precise mental model of the data model and the three transports, byte-level frames you can check by hand, timing and polling arithmetic, a gotcha catalogue, a security posture, and working code.

What this covers: the four-table data model, RTU, ASCII and TCP framing, function and exception codes, serial timing and RS-485 wiring, polling design with numbers, encoding pitfalls, security, gateways to MQTT and OPC UA, and tested Python.

Context and Background

Modbus was published by Modicon in 1979 to let a programmable logic controller talk to its field devices over a serial line. Modicon later became part of Schneider Electric, and the specification passed to the Modbus Organization, which today hosts the documents publicly. The Modbus Organization specification page lists the current set: the Modbus Application Protocol Specification V1.1b3, the Modbus Messaging on TCP/IP Implementation Guide V1.0b, the Modbus over Serial Line Specification and Implementation Guide V1.02, and a separate Modbus Security Protocol document.

That split is worth internalising because it mirrors the architecture. The application protocol defines function codes and data semantics once. Everything below that, serial framing with a checksum or an Ethernet envelope, is a transport that carries the same protocol data unit, or PDU. Engineers who learn Modbus as “RTU” or “TCP” as if they were two protocols tend to be confused when a gateway translates one into the other. They are one protocol with two or three wrappers.

The Modbus protocol is also deliberately small. There is no discovery, no subscription, no timestamps, no data types beyond bits and 16-bit words, and no session. A client sends a request and a server answers. The current specification uses “client” and “server” for what older material calls master and slave, and this article follows that convention except where a vendor manual uses the older words.

Where does the Modbus protocol sit among modern industrial protocols? OPC UA offers typed, discoverable, secure information models, and MQTT offers lightweight publish-subscribe transport for telemetry. Neither displaces Modbus at the device level, because the device is the thing that ships with the interface. A variable-frequency drive, an energy meter or a boiler controller exposes Modbus because it costs a few kilobytes of firmware and no licence. The pragmatic job for an IIoT architect is not to replace it but to contain it: speak Modbus at the edge, convert to typed data as early as possible, and never let raw register numbers leak into the analytics layer.

This sits naturally inside the broader Industrial IoT stack and the classic automation hierarchy described in the ISA-95 and ISA-99 standards. Most Modbus servers you meet are PLCs or devices directly beneath them at Level 1 and Level 0, and most clients are PLCs, gateways, or SCADA drivers at Level 2.

The specification is free to download, which is why it persists and also why implementations vary. Two devices can both be “Modbus compliant” and differ on maximum registers per request, response time, supported function codes and how they treat an unmapped address. The specification defines the grammar. The device manual defines the dialect, and the dialect is what you integrate against.

The Core Architecture: One Protocol, Four Tables, Three Transports

Direct answer: Modbus is a request-response protocol in which a client sends a function code and parameters to a server, and the server returns data from one of four tables: coils, discrete inputs, holding registers and input registers. The same PDU travels inside RTU frames over serial lines with a CRC-16, ASCII frames with an LRC, or TCP segments behind a 7-byte MBAP header on port 502.

Modbus protocol architecture showing one application layer carried over RTU, ASCII and TCP transports into four data tables

Figure 1: One Modbus application layer, three transports, and a common server data model.

Figure 1 shows the layering. At the top the client forms a function code and its arguments. A transport wraps that PDU. RTU and ASCII travel over a shared serial bus, typically RS-485, with up to 247 addressable servers. TCP travels over Ethernet to an IP address, with an extra “unit identifier” byte that only matters when the target is a gateway to a serial bus behind it. At the bottom every server presents the same four tables.

The data model: four tables, not one memory

The data model is the part newcomers misread most. Modbus does not expose “memory”. It exposes four logically separate tables, each with its own address space starting at zero in the protocol, and each reached by specific function codes.

Table Width Access Typical use Read FC Write FC
Coils 1 bit read/write relay outputs, enable flags 01 05, 15
Discrete inputs 1 bit read-only limit switches, alarm contacts 02 none
Input registers 16 bit read-only measurements, status words 04 none
Holding registers 16 bit read/write setpoints, configuration, many measurements 03 06, 16, 22, 23

Because the tables are separate, address 0 in the coil table and address 0 in the holding table are unrelated locations. Many devices alias them to the same underlying memory, and many do not. The device manual is the only authority.

There is a legacy numbering convention that predates the specification’s clean separation. Coils were written as 0xxxx, discrete inputs as 1xxxx, input registers as 3xxxx and holding registers as 4xxxx, so “40001” means holding register at protocol address 0. Vendors differ on whether their documentation shows the one-based reference (40001) or the zero-based wire address (0), and some show a one-based address without the prefix. This off-by-one is the single most common cause of “my values are shifted by one register”. The rule that protects you: derive the wire address from a value you can verify, such as a serial number or a known setpoint, before trusting any table.

The basic unit is the 16-bit register, transmitted big-endian: the most significant byte first. Anything wider than 16 bits is a convention layered on top by the device vendor, which we return to in the pitfalls section. The data-model figure below ties the tables to function codes and to the multi-register encoding problem.

Modbus registers data model with coils, discrete inputs, holding and input registers feeding 32-bit float word order

Figure 2: The four Modbus tables, their function codes, and where multi-register values create word-order ambiguity.

Why the design is stateless, and what that buys

A server holds no session state about its client. Each request carries everything needed to answer it. If a frame is corrupted or lost, the client times out and asks again. There is no handshake to recover and no sequence number to resynchronise on the serial variants.

This is a robust property on electrically noisy plant floors, where variable-frequency drives, welders and switching supplies corrupt frames routinely. It also explains the main limitation. With no push mechanism, anything that changes must be discovered by polling, so latency is bounded below by the poll period and bandwidth is spent re-reading values that did not change. Plant designers accepted that trade in 1979, and it remains acceptable when the number of points is modest and the cycle time is tolerable. It stops being acceptable when someone points a cloud connector at a 40-device bus and asks for one-second updates, a case we quantify later.

Three transports compared at a glance

Modbus RTU is the compact binary serial format and dominates in the field. Modbus ASCII encodes each byte as two hexadecimal characters, which is slower but human-readable and tolerates long gaps between characters. Modbus TCP, formally “Modbus Messaging on TCP/IP”, drops the address and checksum in favour of the MBAP header and relies on TCP and Ethernet for integrity.

Property RTU ASCII TCP
Medium RS-485 / RS-232 RS-485 / RS-232 Ethernet
Encoding binary hex characters binary
Frame start 3.5-character silence colon MBAP header
Frame end 3.5-character silence CR LF length field
Integrity CRC-16 LRC TCP and Ethernet FCS
Max frame 256 bytes 513 characters 260 bytes
Addressing 1-247 on the bus 1-247 on the bus IP, plus unit ID
Concurrency one transaction on bus one transaction on bus multiple connections, transaction IDs

The maximum PDU is 253 bytes in all cases. RTU adds one address byte and two CRC bytes for 256. TCP adds the seven-byte MBAP header for 260.

Deeper Analysis: Frames, Function Codes, Timing and Wiring

This section works through the wire format byte by byte, because debugging Modbus in the field is almost always a matter of reading a hex dump. Everything below can be checked with the short scripts included later.

RTU framing and the CRC

An RTU frame is: server address (1 byte), function code (1 byte), data (0 to 252 bytes), CRC-16 (2 bytes). The CRC is the Modbus variant, polynomial 0xA001 in its reflected form, initial value 0xFFFF, and the result is transmitted low byte first. That byte order is the opposite of every other field in the protocol and is a classic source of CRC failures in hand-written code.

A worked example. To read two holding registers starting at address 0 from server 1, the client builds: 01 03 00 00 00 02, then appends the CRC C4 0B. The full frame is 01 03 00 00 00 02 C4 0B. The server replies with address, function, byte count, the register data, and its own CRC. If register 0 holds 0x4248 and register 1 holds 0x0000, the reply is 01 03 04 42 48 00 00 6E 5D. The byte count (04) is the number of data bytes, not the number of registers.

Write single register (function 06) is the simplest write. Setting register 1 to 3 from server 1 is 01 06 00 01 00 03 98 0B, and a successful server echoes the request back verbatim. Echo is the confirmation. Function 16 (write multiple registers) instead returns the starting address and quantity written.

How an RTU frame is delimited: silence, not a marker

RTU has no start or end character. A frame is delimited by silence on the line: a gap of at least 3.5 character times marks the end of a frame and the start of the next, and a gap longer than 1.5 character times inside a frame means the frame is broken and must be discarded. A character in RTU is 11 bits: start bit, eight data bits, a parity bit and a stop bit, or two stop bits when no parity is used.

The arithmetic is simple and worth doing for your own baud rate. At 9600 bit/s, one character is 11 / 9600 = 1.146 ms, so 3.5 characters is about 4.0 ms. At 19200 bit/s, 3.5 characters is about 2.0 ms. The serial line specification fixes the timings above 19200 bit/s at 1.750 ms for the inter-frame gap and 0.750 ms for the inter-character limit, because timer resolution on microcontrollers makes exact scaling impractical. That means at 115200 bit/s the mandatory silence is 1.75 ms, which is about 18 character times, not 3.5. Nothing is gained by raising baud rate to speed up a polling cycle beyond the point where the fixed gaps dominate.

The practical consequence is that RTU needs a real-time-ish serial driver. On a general-purpose operating system, a USB-serial adapter and a busy scheduler can insert a gap of several milliseconds in the middle of a frame, and the server drops it. This explains a large share of the “works on the bench, flaky on the gateway” tickets. Hardware with a proper UART FIFO and kernel buffering, or a driver that assembles the frame before sending, avoids it.

ASCII framing in one paragraph

ASCII frames start with a colon, carry the same address, function and data bytes written as two uppercase hexadecimal characters each, then a one-byte LRC, and end with carriage return and line feed. The LRC is the two’s-complement of the 8-bit sum of the message bytes. Because the delimiters are explicit, ASCII tolerates gaps up to a second between characters, which suits slow radio and modem links. It uses roughly twice the line time of RTU, and almost every modern device that offers it also offers RTU. Treat ASCII as a compatibility option only.

TCP framing and the MBAP header

Modbus TCP replaces address and CRC with the Modbus Application Protocol header: transaction identifier (2 bytes), protocol identifier (2 bytes, always 0), length (2 bytes, the count of following bytes including the unit identifier), and unit identifier (1 byte). Then comes the same PDU.

The same read of two registers from unit 1 looks like: 00 01 00 00 00 06 01 03 00 00 00 02. The transaction ID 00 01 is chosen by the client and echoed by the server so a client can match replies to requests. The length is 6: one unit byte, one function byte, two address bytes, two quantity bytes. There is no CRC because TCP’s checksum and the Ethernet frame check sequence already protect the segment.

The unit identifier is the least understood field. A native Modbus TCP device usually ignores it or requires a fixed value such as 0xFF or 1. A gateway uses it as the downstream serial address. That is why a client sending unit ID 0 to a gateway can unexpectedly address a broadcast on the serial side or get no response at all. Know whether you are talking to a device or a gateway before choosing the value.

The registered TCP port is 502. Servers may accept several simultaneous connections, but small embedded devices often support only a handful, and a few support one. Open one connection per device and keep it open, rather than reconnecting for every poll, which burns the device’s connection table and adds a TCP handshake to every reading.

Function codes you will actually use

The specification defines public function codes in three categories. In practice, a handful cover nearly everything.

Code Name Notes
01 Read Coils up to 2000 bits per request
02 Read Discrete Inputs up to 2000 bits
03 Read Holding Registers up to 125 registers
04 Read Input Registers up to 125 registers
05 Write Single Coil ON is 0xFF00, OFF is 0x0000
06 Write Single Register echo response
15 (0x0F) Write Multiple Coils up to 1968 bits
16 (0x10) Write Multiple Registers up to 123 registers
22 (0x16) Mask Write Register AND/OR mask, one register
23 (0x17) Read/Write Multiple Registers combined, write then read
43 / 14 (0x2B / 0x0E) Read Device Identification vendor, product code, revision

The limits follow from the PDU ceiling of 253 bytes. A read response carries one function byte, one byte count and the data, so 125 registers is 250 data bytes, which fits. The limit of 125 is a protocol maximum, not a device guarantee, and many devices cap lower. Always check the manual and design the poller with a configurable maximum.

Function 22, mask write, deserves more use than it gets. Changing one bit of a shared control word with read-modify-write over 03 and 06 races against the device’s own logic updating other bits. Mask write lets the server apply the change atomically: the result is (current AND and_mask) OR (or_mask AND NOT and_mask).

Exception responses

When a server cannot satisfy a request it replies with the function code with the high bit set (function OR 0x80) followed by a one-byte exception code. A failed read of holding registers returns function 0x83. The codes that matter:

Code Name What it usually means
01 Illegal Function device does not implement that function
02 Illegal Data Address some address in the range is not mapped
03 Illegal Data Value quantity or value out of range
04 Server Device Failure unrecoverable error while processing
05 Acknowledge long operation accepted, poll for completion
06 Server Device Busy retry later
0A Gateway Path Unavailable gateway cannot route to the target
0B Gateway Target Failed to Respond downstream device silent

Two codes are the daily bread of integrators: 02 and 0B. Exception 02 on a block read often means that one address in the range is unmapped, so the entire request fails rather than returning partial data. That has a direct effect on poll planning, covered below. Exception 0B from a gateway means the gateway tried and the serial device did not answer, which is a wiring, address or baud problem, not a TCP problem.

Modbus gateway sequence translating a TCP MBAP request into an RTU frame with CRC and back

Figure 3: A Modbus TCP request crossing a gateway to an RTU server. The gateway swaps the MBAP header for an address and CRC, and serialises access to the bus.

Figure 3 shows what a gateway does and the constraint it imposes. The MBAP transaction ID and unit ID are held while the serial exchange completes. Because the serial bus is half-duplex and shared, the gateway can have only one request in flight per bus, no matter how many TCP clients are connected. A second client simply queues behind the first, and the effective response time rises with every additional consumer. This is why “just add another dashboard” can slow down the control loop reading the same RS-485 segment.

RS-485 wiring and termination

Most Modbus RTU runs on RS-485, a differential multidrop standard (TIA/EIA-485). Two signal conductors, conventionally labelled A and B (and sometimes D- and D+, with inconsistent polarity naming between vendors), carry the signal as a voltage difference, which gives strong common-mode noise rejection. A reference or ground conductor is carried too, because transceivers have a limited common-mode range and ground potential differences between cabinets can exceed it.

The topology is a daisy-chain, not a star. Stubs should be as short as practical. The line needs a termination resistor of about 120 ohms at each of the two physical ends of the bus, matching the cable’s characteristic impedance, and not at intermediate nodes. Fail-safe bias resistors, pulling the idle line to a defined state, are fitted at one point on the bus so that the line does not float and look like a stream of garbage characters between frames. The Modbus serial line guide discusses bias values in the several-hundred-ohm range, and many gateways provide switchable termination and bias; use the one device that provides it, not several.

The original RS-485 loading allows 32 unit loads on a segment. Modern low-load transceivers allow many more, and repeaters extend further. Cable length depends on baud rate and cable quality: the commonly quoted figure is about 1,200 m at low data rates, falling with speed. Treat those figures as design starting points and verify with the cable and transceiver datasheets.

Typical RS-485 symptoms and causes:

  • Intermittent CRC errors on the farthest device: missing or doubled termination.
  • All devices fail when one device powers up: missing fail-safe bias, or a node with a faulty transceiver holding the bus.
  • Errors that correlate with a drive starting: unshielded cable beside power cables, or shield grounded at both ends with a ground loop.
  • Works at 9600, fails at 115200: stub lengths and reflections.

A timing and capacity model

Poll time is the product of per-transaction cost and the number of transactions. For RTU, a transaction costs the request characters, the server turnaround, the response characters and the mandatory silences. The model below is illustrative arithmetic, using an assumed 5 ms server turnaround.

Consider reading 10 holding registers: the request is 8 bytes and the response is 5 + 20 = 25 bytes, 33 characters of 11 bits.

Baud Time on wire Two silences Turnaround (assumed) Per transaction
9600 37.8 ms 8.0 ms 5 ms about 51 ms
19200 18.9 ms 4.0 ms 5 ms about 28 ms
115200 3.2 ms 3.5 ms 5 ms about 12 ms

Now scale that to a bus of 32 devices, each read for 20 registers (53 characters per transaction). At 9600 bit/s the cycle is roughly 32 times 74 ms, or about 2.4 seconds. At 19200 it is about 1.3 seconds. At 115200 it is about 0.43 seconds. Those numbers are the real answer to “why can’t I get one-second updates from this bus”: the arithmetic forbids it unless you cut registers, raise baud rate, or split the bus into more segments with more gateways.

Deeper Analysis: Encoding, Polling Design, Security and Integration

Reading values correctly: signedness, scaling and word order

A register carries 16 bits and nothing else. Whether those bits are a signed integer, an unsigned integer, a bit field or half of a float is purely a device convention. The pitfalls fall in a predictable list.

Signedness. A temperature of minus five degrees stored as two’s-complement 16-bit reads as 65531 if decoded unsigned. Decode by the documented type, not by default.

Scaling. Devices avoid floating point by storing scaled integers: a register value of 215 for 21.5 degrees (scale 0.1), or a value in milliamps where the process variable is amps. The scale factor lives in the manual. Keep it in the register map, not in code scattered across dashboards.

Bit fields. Status words pack alarms into bits. Define each bit’s name and polarity (some are active-low) in the map.

Multi-register values. A 32-bit float or integer spans two consecutive registers. The two bytes inside each register are always big-endian, but the order of the words is a vendor choice. Take the IEEE 754 float 50.0, which is 0x42480000. Sent as register 0 = 0x4248 and register 1 = 0x0000, that is the “big-endian word order”, often written ABCD. A device that sends the low word first sends register 0 = 0x0000 and register 1 = 0x4248, written CDAB. Decoding that CDAB data as ABCD yields 0x00004248, a denormal float near 2.4e-41, and no error is ever raised; in other cases the wrong order produces a plausible-looking but wrong number. There are also byte-swapped variants (BADC, DCBA) from devices that swap bytes within registers.

The defence is a test vector. For every device model, read a point with a known value, such as a setpoint you wrote yourself, and confirm the decode before any production use.

Torn reads. If a 32-bit counter is split across two registers and you read them in two separate requests, the counter can roll over between the reads and produce a value that was never true. Read both words in one request. Even within one request, firmware that updates the two words non-atomically can briefly tear them; for critical counters, read twice and compare, or use a device-provided latched snapshot.

Poll planning: coalesce, but mind the holes

The cost model above says the number of transactions dominates, so a poller should merge adjacent points into block reads. The second half of the trade-off is the exception 02 risk: if a block spans an unmapped register, the whole request fails.

A good planner therefore merges points when the gap is small, enforces a maximum block size per device, and has a fallback that splits a failing block to isolate the bad address. The tested planner below implements the first two.

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    name: str
    address: int      # zero-based protocol address
    words: int = 1

def plan_blocks(points, max_regs=125, max_gap=4):
    """Merge nearby points into as few read requests as possible.
    A gap of up to max_gap unused registers is cheaper to read than to
    pay another request-response round trip."""
    pts = sorted(points, key=lambda p: p.address)
    blocks, cur = [], None
    for p in pts:
        end = p.address + p.words
        if cur and p.address - cur["end"] <= max_gap and end - cur["start"] <= max_regs:
            cur["end"] = max(cur["end"], end)
            cur["points"].append(p)
        else:
            cur = {"start": p.address, "end": end, "points": [p]}
            blocks.append(cur)
    return [(b["start"], b["end"] - b["start"], b["points"]) for b in blocks]

pts = [Point("temp", 0, 2), Point("press", 2, 2), Point("flow", 10, 2),
       Point("state", 200), Point("alarm", 201)]
for start, count, ps in plan_blocks(pts):
    print(start, count, [p.name for p in ps])
# 0 4 ['temp', 'press']
# 10 2 ['flow']
# 200 2 ['state', 'alarm']

Beyond block planning, tier the poll rates. A temperature that changes over minutes does not need the cadence of a motor current. A common scheme is a fast group read every cycle, a medium group every fifth cycle and a slow group, such as device identification and configuration, once at start-up and on reconnect. With the capacity model above, tiering is often the difference between a bus that works and one that saturates.

Finally, use the response timeout deliberately. A timeout shorter than the slowest legitimate reply turns a slow device into a flood of retries; one that is much longer makes a dead device stall the whole bus for every cycle. Track per-device failure counts, back off a device that has failed repeatedly, and keep polling the others. A single dead server should never be able to slow down thirty-one healthy ones.

Working code: hand-built frames and a modern client

The following script builds an RTU frame and verifies the CRC example from earlier. It uses only the standard library and was run to produce the output shown.

import struct

def crc16(data: bytes) -> int:
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
    return crc

def rtu_frame(addr, fc, payload):
    body = bytes([addr, fc]) + payload
    return body + struct.pack("<H", crc16(body))   # CRC is low byte first

print(rtu_frame(1, 3, struct.pack(">HH", 0, 2)).hex(" "))
# 01 03 00 00 00 02 c4 0b
print(rtu_frame(1, 6, struct.pack(">HH", 1, 3)).hex(" "))
# 01 06 00 01 00 03 98 0b

For TCP, a raw-socket client makes the MBAP header concrete. It was tested against a local server returning registers 0x4248, 0x0000 and 215, with address 100 unmapped.

import socket, struct

def mbap_request(tid, unit, fc, payload):
    pdu = bytes([fc]) + payload
    return struct.pack(">HHHB", tid, 0, len(pdu) + 1, unit) + pdu

def read_holding(sock, tid, unit, start, count):
    sock.sendall(mbap_request(tid, unit, 3, struct.pack(">HH", start, count)))
    r_tid, proto, length, r_unit = struct.unpack(">HHHB", sock.recv(7))
    pdu = b""
    while len(pdu) < length - 1:
        pdu += sock.recv(length - 1 - len(pdu))
    assert r_tid == tid and proto == 0
    if pdu[0] & 0x80:
        raise RuntimeError(f"exception {pdu[1]:#04x} for fc {pdu[0] & 0x7F}")
    n = pdu[1]
    return list(struct.unpack(f">{n // 2}H", pdu[2:2 + n]))

with socket.create_connection(("127.0.0.1", 5020), timeout=2) as s:
    print(read_holding(s, 1, 1, 0, 3))      # [16968, 0, 215]
    try:
        read_holding(s, 2, 1, 100, 1)
    except RuntimeError as e:
        print(e)                            # exception 0x02 for fc 3

In production you would use a maintained library rather than your own socket code. With pymodbus, the keyword that selects the target server changed across releases. Versions through 3.8.x used slave=, and later releases use device_id=, a rename the maintainers made at the Modbus Organization’s request. Check the documentation for the version you pin. The following was run against pymodbus 3.15.0.

import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def main():
    async with AsyncModbusTcpClient("127.0.0.1", port=5020, timeout=2, retries=1) as c:
        rr = await c.read_holding_registers(0, count=3, device_id=1)
        if rr.isError():
            raise RuntimeError(rr)
        temp = c.convert_from_registers(
            rr.registers[0:2], c.DATATYPE.FLOAT32, word_order="big")
        print(rr.registers, temp)           # [16968, 0, 215] 50.0
        bad = await c.read_holding_registers(100, count=1, device_id=1)
        print(bad)  # ExceptionResponse(... function_code=131, exception_code=2)

asyncio.run(main())

Notice isError(): pymodbus returns exception responses as objects rather than raising, so a loop that forgets to test them will happily treat an exception as data. Also pin the library version. The pymodbus server-side datastore classes are deprecated in the 3.x line in favour of a simulator model scheduled to be the only option in v4, so server code written for tests today will need migration.

Security: Modbus has none, by design

The classic Modbus protocol has no authentication, no authorisation and no encryption. Anyone who can reach port 502 can read every table and write every coil and register the device allows. Function code 05 or 06 from a laptop on the plant network is a valid, accepted command. Replay is trivial because there is nothing to bind a frame to a session.

The honest posture for the Modbus protocol follows from that fact. Do not expose Modbus to untrusted networks, ever. Segment it into its own zone, as the zones and conduits model in IEC 62443 and ISA-99 advises, and force traffic through a boundary you control. Allow-list the permitted client IP addresses on the device or the switch where the device supports it. Use industrial firewalls or deep-packet inspection that understand function codes, so that write functions can be blocked from everything except the controller that owns the process. For read-only monitoring, a data diode or a span port that cannot transmit removes the write risk entirely.

There is also a protocol-level answer. The Modbus Organization publishes a Modbus Security Protocol document, commonly called Modbus/TCP Security, which wraps the Modbus TCP exchange in TLS with X.509v3 certificate authentication and uses TCP port 802 instead of 502. Per the Modbus Organization, this is the port the security protocol uses. Mutual authentication means both client and server prove identity, and the design carries an authorisation role with the client certificate so a server can restrict functions per role.

The realistic caveat is adoption. Few field devices ship with it, and those that do add certificate lifecycle work: issuing, rotating and revoking certificates on devices that may have no clock and little storage. The practical pattern for most estates is to terminate security at a gateway: legacy devices speak plain Modbus on an isolated segment, and an edge gateway offers a TLS-protected interface or translates to a secured protocol upstream. Treat the secure variant as the target for new device procurement and a gateway as the bridge for the installed base.

From register maps to a digital twin: gateways to MQTT and OPC UA

Modbus values must acquire meaning before they are useful to a digital twin. A twin needs an asset identity, engineering units, a timestamp, a quality flag, and a stable name. Modbus supplies none of these. The edge gateway supplies all of them, from a register map that is the real schema of the system.

Edge gateway pipeline from Modbus devices through poll planner, decoder and quality stamp to MQTT and OPC UA for a digital twin

Figure 4: A Modbus-to-twin pipeline. The register map drives decoding, quality and timestamping before data reaches MQTT, OPC UA or a historian.

The central idea of this article is to treat the register map as versioned code. It should be a file in a repository, reviewed, tested against a simulator, and tied to a device model and firmware revision. Each row defines the table, wire address, word count, data type, word order, scale, unit, access mode, and the semantic name the twin will use. When a firmware update moves a register, you change one reviewed file, not twelve dashboards.

The pipeline in Figure 4 does five jobs. The poll planner issues coalesced reads. The decoder applies type, scale and word order. A quality stage marks a value good only if the transaction succeeded within its timeout and flags stale or bad otherwise, because a last-known-good value shown without a flag is worse than a gap. A timestamp is applied at read time, with the honest caveat that it is the time the gateway received the value, not the time the sensor sampled it. Finally the publisher emits typed data.

For publication, MQTT suits telemetry fan-out to many consumers, and the Sparkplug specification layered on MQTT adds a defined topic structure and birth and death certificates that convey device state. OPC UA suits the case where consumers need a browsable address space with types and units, and where its security model is required. Many plants run both from the same gateway: OPC UA toward the MES and supervisory systems, MQTT toward the cloud. Whichever you pick, avoid publishing raw register numbers upstream. The decoupling pays off when you swap a Modbus meter for an OPC UA one and nothing above the gateway changes.

Write paths deserve a policy. A twin that can write setpoints through a gateway into a Modbus coil or register has direct reach into physical equipment. Define which points are writable, bound each by range, require a dual control or approval flow for critical ones, and log every write with the originating identity. Modbus itself will not do any of that for you.

Trade-offs, Gotchas, and What Goes Wrong

Modbus makes honest trade-offs, and most field failures trace back to forgetting one of them.

Polling versus change-driven data. With no push, bandwidth scales with points times cycle rate, regardless of how much changes. A 32-device bus at one-second refresh is infeasible at 9600 bit/s, as computed above. The remedies are more segments, fewer or tiered points, or devices that support an event or report-by-exception model. Plain Modbus has none.

Half-duplex serialisation. One transaction at a time per serial bus means every consumer slows every other. Gateways that fan TCP clients into one bus hide the contention until a second system is added and control-loop reads start to jitter.

No semantics. The protocol cannot tell you that register 7 is a pressure in kilopascals. The consequence is not a protocol error but a silent wrong number. Mitigate with the versioned register map, a known-value test per device, and range checks on decoded values at the gateway.

Broadcast and write hazards. Address 0 is broadcast: every server acts and none replies, so the client cannot know the write succeeded. After a broadcast, the client must wait a turnaround delay before the next request, typically on the order of 100 to 200 ms in the serial line guidance. Avoid broadcasts for anything that matters.

Cleartext everywhere. The Modbus protocol ships without access control, so anyone who can sniff or reach the network can read and write. Segmentation and application-aware filtering are mandatory controls, not hardening extras.

The pattern behind all of these is that Modbus pushes responsibility to the integrator. That is the price of its simplicity, and it argues for an architecture in which a small, well-tested edge layer absorbs the protocol’s rough edges once instead of every consumer rediscovering them.

Practical Recommendations

When you adopt the Modbus protocol in a new IIoT design, start from the register map, because everything else depends on it. Create one machine-readable file per device model, version it with the device firmware revision, and include a verification point for each device, a register whose true value you can independently confirm. Treat that file as the contract between the plant and the twin.

For serial networks, choose RTU over ASCII unless a legacy link requires otherwise. Wire a daisy-chain with a shielded twisted pair and a reference conductor, terminate both physical ends at roughly 120 ohms, and apply bias at exactly one point. Run at the lowest baud rate that meets your cycle time, since extra speed buys little above the fixed silence at higher rates, and confirm timings with the capacity arithmetic from this article before committing.

For Ethernet, prefer native Modbus TCP with one persistent connection per device, a sensible timeout and a bounded retry policy. When you must front serial devices with a gateway, set unit IDs deliberately and expect a single in-flight request per serial bus.

Put a poll planner in front of everything. Coalesce adjacent registers with a configurable gap, cap block size per device, tier the rates, and fall back to splitting a block on exception 02. Decode at the edge with type, scale and word order from the map, apply a quality flag, and publish typed data upward over MQTT or OPC UA.

For security, assume Modbus is cleartext and open. Segment it, allow-list clients, filter write function codes, and monitor for unexpected sources. Specify Modbus/TCP Security on port 802 for new equipment, and front legacy devices with a gateway.

Checklist before go-live:

  • Register map reviewed, versioned, and tied to firmware revision.
  • Known-value test passed for every device model, including word order.
  • Capacity calculated for the slowest bus at the planned baud rate.
  • Termination at both ends and a single bias point confirmed.
  • Per-device timeouts, retry caps and back-off configured.
  • Write points enumerated, range-limited, authorised and logged.
  • Modbus zone segmented, with only named clients allowed to reach it.
  • Quality and timestamp semantics documented for downstream users.

Frequently Asked Questions

What is the difference between Modbus RTU and Modbus TCP?

Both carry the same Modbus function codes and data model. RTU is a compact binary format over serial lines such as RS-485, uses a server address and a CRC-16, and delimits frames by silence of at least 3.5 character times. Modbus TCP runs over Ethernet on port 502, replaces the address and CRC with a 7-byte MBAP header, and relies on TCP and Ethernet checksums for integrity. TCP also allows multiple connections, while an RTU bus carries one transaction at a time.

How many devices can be on one Modbus RTU bus?

The protocol addresses servers 1 to 247, with 0 reserved for broadcast. The electrical limit is lower: classic RS-485 allows 32 unit loads on a segment, so 32 standard devices, while low-load transceivers and repeaters allow more. The practical limit is usually timing. Each added device lengthens the polling cycle, so a bus that is electrically fine can be too slow for the update rate you want. Calculate the cycle time before adding devices.

Why do my Modbus values come out shifted or wrong?

Three causes cover most cases. First, off-by-one addressing: vendor documents may show 40001 for wire address 0. Second, word order for 32-bit values: ABCD versus CDAB gives believable but wrong numbers with no error raised. Third, signedness and scaling: a negative value read as unsigned, or a scaled integer used without its factor. Verify with a register whose real value you know, and keep the confirmed decode in a versioned register map.

Is the Modbus protocol secure?

Classic Modbus is not. It has no authentication, authorisation or encryption, so anyone with network access can read and write the device. Mitigate with segmentation, client allow-lists and firewalls that filter write function codes. The Modbus Organization also publishes a Modbus Security Protocol that adds TLS and X.509v3 certificates on TCP port 802, but few field devices implement it, so many sites protect legacy equipment with an edge gateway instead.

Is the Modbus protocol still relevant, or should I move to OPC UA or MQTT?

It is still relevant at the device level because installed equipment speaks it and new low-cost devices keep adding it. The sensible move is not replacement but containment. Keep Modbus on a segmented edge network, decode registers into typed, timestamped, quality-flagged values at a gateway, and publish upward via OPC UA or MQTT. This gives modern consumers rich data while avoiding the cost of replacing working field hardware.

What does Modbus exception code 02 mean?

Exception 02, Illegal Data Address, means the server rejects at least one address in the requested range as not available. For a block read that includes an unmapped register, the entire request fails and returns nothing. It usually indicates a wrong address, an off-by-one error, or a block that spans a gap in the device’s register map. Split the request to find the offending address, or consult the map.

Further Reading

Within this site:

External primary sources:

By Riju — about

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *