Advisory Schedule a Technical Discovery Call — Book your session today! »

· Industrial Protocols  · 11 min read

Modbus RTU & TCP: The Definitive Guide (Protocol, Endianness & MBAP)

Beyond the wire. Master Byte Swapping, Function Codes, and the real structure of Modbus messages for 100% reliable integrations.

Beyond the wire. Master Byte Swapping, Function Codes, and the real structure of Modbus messages for 100% reliable integrations.

Modbus RTU and TCP: one PDU, two transport envelopes

Modbus becomes much easier to troubleshoot when its stable application message is separated from the transport that carries it. RTU and TCP can request the same holding registers, but they establish boundaries, correlate responses, and expose failures differently. This guide owns that protocol architecture: the data model, PDU, RTU frame and CRC, serial timing, TCP MBAP header, gateway behavior, and deployment decisions. It does not prescribe RS-485 cabling or termination; use the Modbus RTU Survival Guide for physical-layer diagnosis. It also does not replace the function-code detail and controlled-write rules in Modbus Function Codes. [M-A-P-001]

Scope, evidence, and safety boundary

The byte examples below are deterministic local simulations, not evidence that a PLC, gateway, serial adapter, or production process was reached. They were run on 2026-07-21 with CPython 3.12.3 and the Python standard library only. No socket, serial port, credentials, device, or third-party Modbus client was used. The resulting receipts prove frame construction and rejection rules, not interoperability, timing margin, or vendor register-map compatibility. Hardware-dependent outcomes are BENCH-ONLY—NOT VALIDATED HERE.

Modbus RTU and Modbus TCP do not provide authentication or authorization. Do not expose a controller, gateway, or serial converter to an untrusted network. Segment OT traffic, use approved remote-access paths, and restrict who can reach port 502 or a serial management interface. Do not discover a production network with broad scans or trial writes. Any permitted write needs an asset-owner-approved procedure, maintenance state, a known rollback value, independent read-back, and qualified personnel. De-energize panels and follow manufacturer instructions, LOTO, and PPE before physical work. This article makes no availability, latency, compliance, or reliability guarantee. [M-A-P-001]

The data model is not the wire address

The Modbus application protocol defines four logical object classes. Coils and discrete inputs are single-bit objects; input registers and holding registers are 16-bit objects. Coils and holding registers may be writable when the server implements the relevant function; discrete inputs and input registers are read-only in the model. The familiar 0x, 1x, 3x, and 4x reference prefixes are documentation conventions. They are not fields transmitted in a request. [M-A-P-001]

Object classTypical reference notationData on the wireTypical access
Coils0xxxxpacked bitsread/write
Discrete inputs1xxxxpacked bitsread
Input registers3xxxx16-bit wordsread
Holding registers4xxxx16-bit wordsread/write

A request PDU carries a function code, then function-specific fields such as a zero-based starting address and quantity. A manual that labels a value 40101 may intend protocol offset 100, while a particular SCADA driver may ask for 40101 or 101. Those are interface conventions, not a universal conversion rule. Confirm the vendor map, the client API’s addressing convention, and one approved read-only vector before commissioning. A successful response proves only that a server answered that request; it does not prove scale, signedness, engineering unit, byte order, or word order. [M-A-P-001]

The application specification uses client and server: the client starts the transaction and the server acts or replies. Historical serial-line guidance also says master and slave. For new integrations, document client/server; inherited terminology neither identifies a network peer nor grants operational authorization.

ADU and PDU: the useful split

The Protocol Data Unit (PDU) is the common application portion: one function-code byte plus data. A read-holding-registers PDU for offset 100 and quantity 2 is 03 00 64 00 02. The Application Data Unit (ADU) wraps that PDU for a particular transport. Keeping the split explicit prevents a common diagnostic error: treating TCP’s header or RTU’s address and CRC as though they were function-code parameters.

For RTU, the ADU is:

unit address | function code + data (PDU) | CRC low byte | CRC high byte

For TCP, the ADU is:

MBAP header (7 bytes) | function code + data (PDU)

The same PDU may therefore appear inside either envelope. A gateway commonly removes the TCP MBAP header, uses the Unit Identifier to select a serial target according to its configuration, sends an RTU ADU downstream, then constructs a TCP response upstream. That translation is not transparent when its timeout, queueing, unit mapping, or serial settings are wrong. [M-A-P-002] [M-A-P-003]

RTU framing, timing, and CRC

RTU places a one-byte server address ahead of the PDU and appends a two-byte CRC calculated over the address and PDU. The CRC is transmitted low-order byte first. It detects many corrupted frames, but it is not security: an attacker or faulty sender can create a valid CRC for unwanted content. Validate it before interpreting a PDU, preserve the original capture when it fails, and do not “repair” bytes in a troubleshooting record. [M-A-P-003]

RTU framing also depends on silence. The serial-line guide defines an inter-frame interval of at least 3.5 character times; a gap greater than 1.5 character times inside a frame makes the receiver treat it as incomplete. At baud rates above 19,200 bit/s, the guide recommends fixed values of 750 microseconds for the inter-character timeout and 1.75 milliseconds for the inter-frame delay. Record baud rate, parity, data bits, stop bits, timing configuration, and both raw ADUs. A capture beginning halfway through a message cannot be safely reconstructed by searching for a recognizable function byte. [M-A-P-003]

Fixture P-RTU-01 is a read-holding-registers request to unit 1: 01 03 00 00 00 0A C5 CD. The first six bytes ask for ten registers from offset zero. Applying the standard Modbus CRC algorithm to 01 03 00 00 00 0A yields numeric CRC 0xCDC5; RTU transmits that numeric value as C5 CD because the low byte comes first. The fixture is deliberately local: it checks ordering and arithmetic, not a serial link. [M-A-P-003]

TCP framing and the MBAP header

Modbus TCP replaces RTU’s address and CRC with the seven-byte MBAP (Modbus Application Protocol) header: two bytes of transaction identifier, two bytes of protocol identifier, two bytes of length, and one byte of Unit Identifier. The PDU follows immediately. The protocol identifier is zero for Modbus. The length counts the Unit Identifier plus the PDU; it does not count the first six MBAP bytes. [M-A-P-002]

MBAP fieldSizeValidation use
Transaction Identifier2 bytesMatch a response to an outstanding request.
Protocol Identifier2 bytesMust be 0x0000 for Modbus.
Length2 bytesMust equal the bytes from Unit Identifier through the PDU.
Unit Identifier1 byteIdentify a downstream unit at many gateways; direct-server meaning is device-specific.

Fixture P-TCP-01 is 00 01 00 00 00 06 11 03 00 64 00 02. It declares transaction 1, protocol 0, length 6, Unit Identifier 17, function 3, offset 100, and quantity 2. The length is six because it covers 11 03 00 64 00 02. A response must be correlated to the current request before its data is decoded; matching only function code and unit is insufficient when a client has multiple outstanding transactions. [M-A-P-002]

TCP’s stream behavior is another boundary. A single recv() is not guaranteed to return a whole ADU, and more than one ADU can arrive in a read. Buffer bytes until the MBAP length yields one complete ADU, reject impossible lengths under an application-defined limit, then retain any remaining bytes for the next message. TCP integrity mechanisms do not replace MBAP, transaction, function, byte-count, or semantic validation. A connected socket says only that the TCP peer accepted a connection.

Unit identifiers and gateway failure domains

The Unit Identifier is straightforward only when the target’s documentation says what it means. In a TCP-to-serial gateway, it commonly selects the downstream serial unit. In a direct TCP server, it may be ignored, fixed, or used by vendor-specific routing. Never assume that 0, 1, or 255 has one universal meaning. Record the exact gateway model, firmware revision, mapping configuration, and target documentation with a commissioning receipt. [M-A-P-002]

A gateway creates separate upstream and downstream failure domains. The upstream client may complete a TCP handshake while the downstream serial port has the wrong parity, an unavailable unit, a timeout, or an electrical problem. Conversely, a valid RTU response can be delayed, translated into an exception, or lost under an upstream timeout policy. Diagnose from the boundary outward: retain the upstream MBAP request and response, gateway log or configuration revision where authorized, downstream RTU capture where safely available, and timestamps. Do not solve an illegal-address exception by increasing retries; the address map or request must change.

Request, response, and exception flow

A normal exchange is request PDU, transport envelope, response envelope, response PDU, then application decoding. For function 0x03, a response starts with 03, a byte count, and register bytes. Check the expected byte count against quantity × 2 before converting values. For multiword values, retain the raw 16-bit words and apply the documented signedness, scale, byte order, and word order in one tested conversion step. 41 C8 00 00, for example, represents IEEE-754 25.0 only under a specified ordering; a different ordering is a different bit sequence, not evidence of a network fault.

An exception response has the request function with bit 7 set, followed by an exception code. A request using function 03 can therefore receive 83 02: exception response to function 3, code 2 (illegal data address). Treat it as a structured protocol result. Illegal function, data address, or data value normally calls for a documented-map or request correction. Only retry failures that the device and deployment policy identify as transient; retries can duplicate traffic and are particularly dangerous around write operations. [M-A-P-001]

Deterministic fixture and receipt

Run the following command exactly in CPython 3.12.3 or another Python 3 standard-library environment. It validates the TCP length, transaction/protocol fields, the RTU CRC vector, a normal byte count, and an exception shape. It also rejects a malformed TCP length. It does not open a network or serial connection.

python3 - <<'PY'
def crc16(data):
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
    return crc

tcp = bytes.fromhex('000100000006110300640002')
assert len(tcp) == 12
assert int.from_bytes(tcp[4:6], 'big') == len(tcp) - 6 == 6
assert tcp[:4] == bytes.fromhex('00010000')
assert tcp[6:] == bytes.fromhex('110300640002')
assert crc16(bytes.fromhex('01030000000A')) == 0xCDC5
normal = bytes.fromhex('110304002A000B')
assert normal[1] == 3 and normal[2] == 4 and len(normal[3:]) == normal[2]
exception = bytes.fromhex('118302')
assert exception[1] == 0x83 and exception[2] == 2
bad = bytes.fromhex('000100000007110300640002')
assert int.from_bytes(bad[4:6], 'big') != len(bad) - 6
print('V-A-P-001 PASS: MBAP, CRC, normal response, exception, malformed length rejected')
PY

Receipt V-A-P-001. Expected output: V-A-P-001 PASS: MBAP, CRC, normal response, exception, malformed length rejected. Observed output on 2026-07-21: exactly that line, exit status 0. Fixture inputs: P-TCP-01, P-RTU-01, normal response 11 03 04 00 2A 00 0B, exception 11 83 02, and malformed-length TCP request. Limitation: this is a simulation-only parser and encoder receipt; it is BENCH-ONLY—NOT VALIDATED HERE and cannot establish device behavior, gateway routing, serial timing, or write safety.

Deployment sequence and RTU-versus-TCP decision

Choose RTU when the device interface is serial and the deployment can control the serial parameters, unit addressing, timing, and physical network. Choose TCP when devices or approved gateways expose Ethernet and operations benefit from IP routing, centralized monitoring, and transaction correlation. TCP does not remove the need to understand the downstream RTU estate; a gateway merely moves that boundary. Prefer a direct TCP server where the documented architecture supports it, but do not replace a stable serial design solely to obtain a familiar socket API.

Start with one approved, read-only point during a maintenance window. Record source document and revision, unit identifier, function, offset, quantity, raw request, raw response, decoded value, expected engineering value, scale, ordering, timestamp, authorization, and rollback boundary. If the first result is implausible, compare map semantics before blaming transport. If RTU CRC failures appear, move to the physical-layer guide; if TCP length or transaction checks fail, inspect buffering, concurrency, and the gateway boundary. Escalate from these simulations to an isolated bench and only then to an authorized site test. No production write follows from this article.

Limitations, references, and next reading

This guide does not cover every vendor register map, encrypted remote-access architecture, gateway queue policy, RS-485 topology, library API, or process-specific interlock. Its fixtures are intentionally small and deterministic so a reviewer can reproduce their limits. Hardware success is not claimed. For cable, termination, biasing, noise, and capture practice, read the Modbus RTU Survival Guide. For function-specific request layouts, packing, response parsing, exception handling, and bounded write controls, read Modbus Function Codes.

References

Last verified: 2026-07-21.

Back to Blog

Related Posts

View All Posts »