Modbus TCP — Won't Connect, or Connects and Lies

beginner · 25 min

You will be able to split a Modbus TCP fault into "no socket" versus "wrong map" in under a minute, then fix the Unit ID, the 40001 off-by-one, the wrong function code, or the swapped 32-bit word order — reading the exception code to tell you which.

ℹ️ Almost every Modbus TCP fault is one of two things, and they need opposite fixes. Either no socket — nothing is listening, so nothing replies — or wrong map — the socket is fine and your understanding of the register list is wrong. One TCP test on port 502 tells you which. If that port opens, put the cable tester down: from there it is a documentation-reading problem, and the single most common one is that the 40001 in the manual is not the number that travels on the wire.

Split the fault in two, first

Two different failures hide behind "Modbus isn't working."

A: no reply. Your client (the master — the device asking for data) times out. The fault is below Modbus: IP, port, firewall, or no server listening.

B: replies, wrong content. You get data, but it is garbage — or you get an exception response, a short error reply from the server (the slave — the device holding the data). The network is fine.

One test separates them. Open a bare TCP socket to port 502, the registered port for Modbus TCP.

  • Windows: Test-NetConnection 192.168.1.50 -Port 502
  • Linux/macOS: nc -vz 192.168.1.50 502

Read how it failed, not just that it failed:

ResultMeaning
Refused immediately (RST)Something answered. Usually nothing listening on 502 — server disabled or on another port. A firewall set to reject rather than drop, and a device that has run out of connection slots, also return RST.
Timeout, silenceWrong IP, wrong subnet or gateway, or a firewall dropping the SYN.
Connects and stays openTransport is fine. The fault is in Modbus itself — Unit ID or data model.

One caution before you repeat this test: many field devices accept only two or four simultaneous TCP connections, and Test-NetConnection and nc each take one. If a laptop, a SCADA server and a historian are already polling, a new connection may be refused or reset. Close your own extra tools and retry. Do not drop a running SCADA or historian connection to free a slot — that is an operational change on a live process, not a diagnostic step, and it needs the control room's agreement first.

Unit ID — and why a gateway changes the rules

Every Modbus TCP request opens with a 7-byte MBAP header: Transaction ID (2), Protocol ID (2), Length (2), Unit Identifier (1). The last byte is the Unit ID.

  • Device directly on Ethernet. The MODBUS Messaging on TCP/IP Implementation Guide V1.0b says to use 0xFF (255) here as a non-significant value, so that if the IP is later reassigned to a gateway, that gateway won't misroute the request onto a serial bus. The same section notes that 0 is also accepted for a device directly on TCP/IP. In practice many devices ignore the byte, most accept 1, and some validate it strictly and answer nothing else. Try 255, then 1, then 0.
  • Device behind a gateway. A gateway (Modbus TCP-to-RTU bridge) treats the Unit ID as the serial slave address, 1 to 247. Here the two failure signatures both exist, and you must read which one you got:
  • A spec-compliant gateway forwards the request, gets nothing back from the serial bus, and returns exception 0x0B, Gateway Target Device Failed to Respond — or 0x0A, Gateway Path Unavailable if it has no route configured for that Unit ID at all. That is the gateway telling you the Ethernet side is fine and the serial side is not.
  • A simpler or misconfigured gateway just forwards and stays quiet, and you see a timeout.

So: clean TCP connect, then either 0x0B/0x0A or silent timeouts, through a gateway, is the classic Unit ID fault. Do not treat silence as the only signature.

Sweep it with mbpoll, where -a sets the Unit ID: mbpoll -m tcp -a 1 -t 4 -r 1 -c 1 192.168.1.50. Loop -a from 1 to 10, then try 255.

Two rules while sweeping. Do not use Unit ID 0 through a gateway — on the serial side 0 is the broadcast address, it is never answered, and a broadcast write reaches every slave on the bus. Reserve 0 for a device connected directly to TCP/IP. And remember that every sweep step puts a frame on a serial bus that is already carrying the plant's own polling; keep the sweep short and the poll rate low.

The rule that saves the most time: any exception response is good news. It proves the TCP path works, the Unit ID was accepted, and a real Modbus server processed your frame. The fault is then entirely in the data model, and everything below applies. Silence means you have not got that far yet — stay on the Unit ID and the transport.

The off-by-one, explained properly once

Three facts, and the confusion dissolves.

1. The only address that ever travels on the wire is a 16-bit number, 0 to 65535, always zero-based.

2. The Modbus data model numbers each block from 1 to n. Register numbered 1 is addressed on the wire as 0. Data numbered X is addressed as X−1.

3. The 4xxxx numbers in vendor manuals are neither. They are the old Modicon reference convention — 0x coils, 1x discrete inputs, 3x input registers, 4x holding registers. The leading digit is a type tag, not part of the number, and this convention is not in the Modbus specification.

So 40001 → wire address 0, and 40108 → wire address 107. Subtract 40001. Not 40000.

Here is what actually bites. Vendors publish this inconsistently. Before writing anything, decide which column you are reading:

  • Headed Register, values from 40001 (or from 1) → subtract.
  • Headed Address, Offset, or PDU address, values from 0 → use as-is.
  • Ambiguous? Poll both ways. In mbpoll, -r is 1-based by default and -0 switches to raw PDU addressing. One returns a sane value, the other returns exception 02 or nonsense. Ten seconds, no theory.

Masters differ in whether they subtract for you — that is where "it works in my laptop tool but not in the PLC" comes from. On Siemens S7-1200/S7-1500, the MB_CLIENT block's MB_DATA_ADDR takes the 4xxxx-style number: with MB_MODE = 0, MB_DATA_ADDR in the range 40001–49999 issues FC03 against remote addresses 0–9998, so MB_DATA_ADDR = 40001 reads wire address 0. Entering a raw 0 there is a different request entirely.

Function code 3 versus 4

Holding registers and input registers are two separate memory blocks, and both start at "register 1."

  • FC03, Read Holding Registers — the 4x block, readable and writable.
  • FC04, Read Input Registers — the 3x block, read-only.

Wire address 100 exists in both, holding different values. Read the wrong block and you get plausible wrong numbers, or exception 02 if the device implements only one. Symptom: a value that sits still and wrong, or reads zero while the device's own display shows it changing. Check the leading digit: 30107 is FC04 at wire 106, 40107 is FC03 at wire 106.

32-bit values across two registers

A register is 16 bits. A 32-bit float or long occupies two consecutive registers. The specification defines the byte order inside a register — most significant byte first — but does not define which register carries the high word. Both orders shipped and both are common.

Diagnose it instead of guessing. Read the pair as raw hex with mbpoll -t 4:hex, then match what you see against a value you can read off the device's own display. For a float of 1.0, whose IEEE 754 encoding is 3F 80 00 00, the four orderings in the field look like this:

OrderingRegister NRegister N+1
Big-endian (ABCD) — as specified3F800000
Byte-swapped (BADC)803F0000
Word-swapped (CDAB)00003F80
Little-endian (DCBA)0000803F

Match the pattern, set that ordering in the master, and re-read to confirm. Do not judge by "the number looks close" — a wrong ordering usually produces a value that is absurd rather than nearly right, and a plausible-looking number from the wrong ordering is a coincidence you cannot rely on. If both registers read plausibly on their own, you are probably looking at two separate 16-bit values, not one 32-bit. Start the read on the boundary the vendor defines; beginning one register early splits every value in the block.

Exception codes name the failing layer

If the response function code equals the request's code plus 0x80 (FC03 → 0x83), the next byte is the exception code.

  • 01 Illegal Function — the device does not support that function code. You sent FC04 to a device that implements only FC03, or you are talking to something that is not a Modbus server.
  • 02 Illegal Data Address — the address, or the combination of address and quantity, does not exist. This is the off-by-one most of the time. It also appears when a valid start address plus your read count runs past the end of the block. Fix the offset first, then the count.
  • 03 Illegal Data Value — the address is fine, a value in the request is not. Quantity outside the allowed range (FC03 and FC04 permit 1 to 125 registers per request), or a write value the device rejects.
  • 04 Server Device Failure — an unrecoverable error inside the device while processing. Not your addressing. Look at the device itself.
  • 0A Gateway Path Unavailable — a gateway could not allocate an internal path from its input port to its output port. The gateway is misconfigured or overloaded; the Unit ID you sent maps to nothing it knows about.
  • 0B Gateway Target Device Failed to Respond — a gateway forwarded your request and the device on the serial side never answered. Wrong Unit ID, wrong serial parameters, or a dead slave. Troubleshoot the serial segment, not the Ethernet one.

Watch it live in Wireshark: display filter mbtcp isolates the Modbus/TCP traffic and modbus.exception_code pulls out the exception byte itself. Use tcp.port == 502 when you also want the handshake.

Step-by-step

  1. Test port 502. Record refused, timeout, or open.
  2. Refused → enable the Modbus TCP server, find its real port, or check whether the device's connection slots are full. Timeout → fix IP, mask, gateway, VLAN, firewall.
  3. Open → poll one register with a known-good tool before touching the PLC: mbpoll -m tcp -a 1 -t 4 -r 1 -c 1 <ip>.
  4. Read what came back before changing anything. Any exception response means the Unit ID and transport are already correct — skip to step 5. Silence means keep working on the Unit ID: sweep 1–10, then 255. Behind a gateway, use the serial slave address, never 0, and keep the sweep short — those frames land on a live serial bus. An 0x0A or 0x0B reply from a gateway is your answer, not a failure.
  5. Read the vendor map and classify the column: 1-based register numbers, or 0-based addresses. Write the answer on the drawing.
  6. Poll one known register both ways — -r N and -0 -r N-1. Keep whichever returns a sane value.
  7. Confirm the block: same address with -t 4 (FC03) and -t 3 (FC04). Match it to the 3x/4x prefix.
  8. For 32-bit values, read the pair as hex and match the four-ordering table against a value shown on the device, then set word and byte order.
  9. Transfer the working parameters into the PLC or SCADA driver one at a time, then capture 30 seconds with the mbtcp filter and confirm zero exception responses.

Never do this

Never fix an addressing fault by adding an offset in the application. It is the tempting move: the value reads one register low, so someone adds +1 to the SCADA tag or the ST program and the screen looks right. The map is still wrong. Every register mapped after that is wrong by one, the next engineer trusts the tag database, and the fault returns as a write — a setpoint, a torque limit, a valve position landing in the neighbouring register on a live process. Fix it in the driver configuration, where the wire address is formed, and correct the documentation the same day.

Never test a write on a running machine. FC06 and FC16 land in the live register file. With a wrong offset you do not get an error, you get a successful write to the wrong register. And never write with Unit ID 0 through a gateway — that is a broadcast to every slave on the serial bus, with no reply to tell you what you just hit.

Never diagnose with a 125-register block read. If any part of the range is invalid the whole request fails with exception 02 and you learn nothing about where. Narrow to one register.

Never disconnect a live SCADA or historian session to free a connection slot without the control room's agreement. Close your own test tools first; a device that only accepts two connections will refuse yours, and that refusal is itself a diagnostic result worth recording.

Key points

  • A Modbus TCP fault is either a socket problem or a register-map problem, and testing TCP port 502 with Test-NetConnection or netcat tells you which one you have before you touch anything else.
  • The 40001-style numbers in vendor manuals come from the old Modicon reference convention and are not part of the Modbus specification, so holding register 40001 is wire address 0 and you subtract 40001, not 40000.
  • Behind a Modbus TCP-to-serial gateway, a wrong Unit ID produces a timeout rather than an exception, because the gateway forwards the request to a serial slave address that nobody answers on.
  • Function code 03 reads holding registers (the 4x block) and function code 04 reads input registers (the 3x block); the same address exists in both and holds different data, so the wrong function code returns plausible wrong numbers.
  • Exception code 02 means the address or the address-plus-quantity does not exist and is almost always the off-by-one, while exception code 04 means the device failed internally and your addressing is not the problem.

Codes and symptoms

02 — Illegal Data Address
The requested address, or the combination of starting address and quantity, is not valid for this device. In the field this is most often the 40001 off-by-one, or a read count that runs past the end of a valid block.
03 — Illegal Data Value
The address is acceptable but a value in the request is not. Commonly a register quantity outside the permitted range — function codes 03 and 04 allow 1 to 125 registers per request — or a write value the device rejects.
01 — Illegal Function
The function code in the request is not supported by the device. Typically sending FC04 to a device that implements only FC03, or polling something that is not a Modbus server at all.
04 — Server Device Failure
An unrecoverable error occurred inside the device while it processed the request. This is a device fault, not an addressing fault — investigate the hardware or the subsystem behind that register.
0A — Gateway Path Unavailable
A gateway could not allocate an internal communication path from its input port to its output port for the request. The gateway is misconfigured or overloaded, or the Unit ID you sent maps to no configured route. Fix the gateway's routing table, not the register map.
0B — Gateway Target Device Failed to Respond
A gateway forwarded the request but no response came back from the target device on the serial side. Wrong Unit ID (serial slave address), wrong serial parameters, or a dead or disconnected slave. This is the exception a compliant TCP-to-RTU gateway returns for a wrong Unit ID — do not assume a Unit ID fault always shows as silence.
0x83 — Exception response to FC03
An exception response echoes the request's function code with the high bit set, so FC03 comes back as 0x83 and FC04 as 0x84. The byte after it is the actual exception code. In Wireshark, the mbtcp filter isolates the traffic and modbus.exception_code extracts that byte. Any exception at all confirms the transport, the Unit ID and the server are working — the fault is in the data model.