The Modbus TCP frame, byte by byte: MBAP header and PDU
· By Vladimir Chemeris, developer of Modbus TCP FieldKit
Short answer: a Modbus TCP frame is a 7-byte MBAP header followed by a PDU of up to 253 bytes. There’s no CRC, because TCP handles integrity. All multi-byte fields are big-endian.
A real request
We read three holding registers from a pymodbus 3.16.1 server with a raw socket:
00 01 00 00 00 06 01 03 00 00 00 03
| Bytes | Field | Value | Meaning |
|---|---|---|---|
00 01 |
Transaction ID | 1 | The client picks it, and the server repeats it |
00 00 |
Protocol ID | 0 | Modbus requires 0 |
00 06 |
Length | 6 | Bytes that follow: Unit ID plus PDU |
01 |
Unit ID | 1 | Routing behind a gateway |
03 |
Function | 3 | Read Holding Registers |
00 00 |
Address | 0 | First register, 0-based |
00 03 |
Quantity | 3 | Three registers |
Everything up to the Unit ID is the MBAP header. The function code and what follows form the PDU.
The response
00 01 00 00 00 09 01 03 06 00 64 00 FA 04 00
| Bytes | Field | Meaning |
|---|---|---|
00 01 |
Transaction ID | Same as the request |
00 00 |
Protocol ID | 0 |
00 09 |
Length | 9 bytes follow |
01 |
Unit ID | Echoed |
03 |
Function | Same function, no error |
06 |
Byte count | 6 data bytes |
00 64 |
Register 0 | 100 |
00 FA |
Register 1 | 250 |
04 00 |
Register 2 | 1024 |
If the function failed, the response would carry 83 and an exception code instead.
What each header field is for
Transaction ID. TCP may deliver responses after the client has moved on. The client matches each response to its request by this number. The implementation guide says a response with a Transaction ID that matches no pending request “must be discarded”. Some RTU to TCP gateways return 0, which is a common reason for strict clients to complain.
Protocol ID. Modbus requires 0. We sent a frame with 00 01 here, and pymodbus stayed silent until the client timed out. That is a hard failure to diagnose without a capture.
Length. The number of bytes after this field. A receiver uses it to find the end of the frame in the TCP stream. If it’s wrong, the client and server lose track of where frames begin.
Unit ID. Selects the device behind a gateway. For a device on TCP with no gateway, the guide recommends 0xFF and also accepts 0. Details and real behaviour.
Size limits
The serial heritage sets the limits. A serial frame tops out at 256 bytes, which leaves 253 for the PDU. Add the 7-byte MBAP header and you get 260 bytes for TCP.
That’s where the familiar quantity limits come from: a read response holds at most 125 registers (250 bytes of data, plus function code and byte count), and a write of multiple registers at most 123 (the request also carries address, quantity and byte count).
Compared with RTU
The same read as an RTU frame swaps the header for a slave address and adds a CRC:
01 03 00 00 00 03 05 CB
There’s no transaction ID, length or protocol field, so an RTU master can have only one request outstanding on the line. RTU over TCP vs Modbus TCP covers what this means with converters.
To read your own traffic, paste it into the frame decoder.
Sources
- Modbus Application Protocol Specification V1.1b3, section 4.1
- Modbus Messaging on TCP/IP Implementation Guide V1.0b, sections 3.1 and 4.4
- Frames captured on 30 October 2026 from pymodbus 3.16.1 on Linux
FAQ
What is the MBAP header?
The 7-byte header that Modbus TCP puts in front of the PDU: Transaction ID (2 bytes), Protocol ID (2), Length (2) and Unit ID (1). It replaces the address and CRC of a serial RTU frame.
Does Modbus TCP have a CRC?
No. TCP already protects the data, so the Modbus TCP frame has no CRC. RTU frames carry a 2-byte CRC, and so does RTU over TCP.
What is the maximum Modbus TCP frame size?
260 bytes: a PDU of up to 253 bytes plus the 7-byte MBAP header. The limit comes from the original 256-byte serial frame.