Modbus register addressing: 40001, 0-based and off-by-one errors
· By Vladimir Chemeris, developer of Modbus TCP FieldKit
Short answer: 40001 means the first holding register, and a Modbus frame carries it as address 0. The 4 picks the area, and the remaining digits count from 1. Subtract one and drop the prefix: 40108 becomes address 107.
Three numbers for one register
The Modbus specification defines two numbering schemes. Section 4.4 of the Application Protocol Specification V1.1b3 says it plainly: “In a MODBUS PDU each data is addressed from 0 to 65535”, while “in the MODBUS data Model each element within a data block is numbered from 1 to n”. An element numbered X travels in the PDU as X-1.
Vendors then add a third habit: the area prefix. That gives you these names for the same register:
| Written as | Style | Means |
|---|---|---|
0 or 0x0000 |
Protocol address | What the frame carries |
1 |
1-based register number | The first element in the block |
40001 |
5-digit reference | First holding register |
400001 |
6-digit reference | First holding register, extended range |
The prefix isn’t in the specification’s frame format. It comes from old Modicon documentation and stuck around because it also names the area.
| Prefix | Area | Read with | Write with |
|---|---|---|---|
0xxxx |
Coils | 01 |
05, 15 |
1xxxx |
Discrete inputs | 02 |
none |
3xxxx |
Input registers | 04 |
none |
4xxxx |
Holding registers | 03 |
06, 16 |
The address converter does this arithmetic for the number you paste.
What the wrong number looks like on the wire
We ran a pymodbus 3.16.1 server with three holding registers at address 0: 100, 250 and 1024. A raw read of address 0, quantity 3, returned all three:
TX 00 01 00 00 00 06 01 03 00 00 00 03
RX 00 01 00 00 00 09 01 03 06 00 64 00 FA 04 00
Then we made the classic mistake and sent 40000 as the address, as if the prefix belonged in the frame. 0x9C40 is 40000:
TX 00 02 00 00 00 06 01 03 9C 40 00 01
RX 00 02 00 00 00 03 01 83 02
83 02 is exception 02, Illegal Data Address. You got lucky here. If the device has registers up there, you read the wrong data and get no error at all.
An off-by-one gives no error
Off-by-one is worse, because the device answers. Ask for address 1 when you wanted address 0, and you get 250 instead of 100. Nothing on screen says so. A 32-bit value is the nastiest case: start one register late and the app combines the low half of one value with the high half of the next, which often looks like a plausible number.
A test that settles it
- Pick a register with a value you can verify: a firmware version, a serial number, or a setpoint you can change on the device’s own display.
- Read a small window around the documented number, for example 5 registers starting two below it.
- Find the known value in the response. Its position tells you the offset.
- Write down the offset for this device family, and keep it in your notes or a saved profile.
Tools disagree about defaults, so check yours. mbpoll starts at reference 1 unless you pass -0 for PDU addressing. In FieldKit you choose 0-based, 1-based or PLC 4xxxx next to the connection and can switch without disconnecting.
The 5-digit limit
Five digits give you 9999 registers per area, but the protocol allows 65536. That is why manuals for larger devices use six digits: 400001 to 465536. If a manual shows 412345, read it as holding register 12345, which is protocol address 12344.
Sources
- Modbus Application Protocol Specification V1.1b3, section 4.4 (addressing model)
- Frames captured on 30 October 2026 from pymodbus 3.16.1 on Linux, using a raw TCP socket
FAQ
Is Modbus 0-based or 1-based?
Both, at different layers. The protocol frame uses addresses 0 to 65535. The data model numbers elements from 1. Under the specification, an element numbered X travels in the PDU as X-1.
What address do I send for holding register 40001?
Address 0, with function code 03. The leading 4 selects the holding register area and is not part of the address sent on the wire.
How do I find out which numbering a manual uses?
Read one register whose value you know, such as a serial number or a setpoint you can change on the device. Try the documented number, then one lower. The one that returns the known value tells you the numbering.