Modbus TCP
Top 10 Key Ideas
- Register-based, not object-based — you're reading/writing numbered memory addresses, not named data points
- Function codes are verbs — every transaction is a 'read X registers starting at Y' or 'write Z to register Y'
- Unit ID is not the same as the IP address — one TCP connection can address multiple logical devices behind a gateway
- Holding registers (4xxxx) vs. input registers (3xxxx) — holding are read/write, input are read-only
- Big-endian within each 16-bit register — the spec defines no word order for 32-bit values, so devices differ, and the wrong word order yields a wrong value
- No built-in security in classic Modbus TCP — authentication, encryption, and access control must come from the network layer
- Polling-only — the client asks, the server answers. No unsolicited reporting, no event-driven updates
- Connection management matters — each TCP connection consumes a slot on the device. With too many connections, the device refuses new ones or closes an old one
- Register maps are vendor-specific — '40001' on an SEL relay means something completely different from '40001' on a Shark meter
- Modbus TCP carries the same PDU as Modbus RTU, with an MBAP header in place of RTU's address and CRC — same function codes, same register model, different transport
ELI5
Imagine a warehouse with numbered shelves. You can ask the warehouse worker: “What’s on shelf 100?” or “Put this box on shelf 200.” That’s Modbus. Every device has numbered shelves (registers), and you read from them or write to them by number. The worker only answers when you ask — they never volunteer information on their own.
Outsider’s Guide
Modbus is one of the most widely supported protocols on power and building equipment. If your project includes power meters, generator controllers, UPS systems, or building automation, chances are at least some of them speak Modbus.
Where it sits: Between the SCADA/gateway layer and field devices. The gateway (like an SEL RTAC) polls each device periodically: “give me your current readings.” The device responds with register values. The gateway aggregates this data for the operator HMI or historian.
Where we have used it: Our team has engineered Modbus TCP in production power-system control, status, and metering integrations (UPS interfaces included) as one layer of a multi-protocol P&C system.
What a prime contractor should care about: Modbus is reliable and widely supported, but it’s polling-based — the gateway asks, the device answers. For monitoring and non-critical control, this is fine. For time-critical protection (load shedding, interlocking), the polling delay makes Modbus too slow — that’s where GOOSE takes over.
Red flag from a sub: A proposal that puts critical interlocking or load shedding on Modbus TCP polling has to answer one question: how does a 500ms-2s poll cycle meet the required response time? Modbus belongs on the monitoring and non-critical control layer; the control-critical path belongs on GOOSE.
Visual Explanation
Where Modbus TCP Sits in a SCADA System
┌─────────────────────────────────────────────────────────┐
│ SCADA / HMI / Historian │
│ (Modbus TCP Client — the "master") │
└──────────────┬───────────────┬───────────────┬──────────┘
│ │ │
TCP port 502 TCP port 502 TCP port 502
│ │ │
┌───────▼──────┐ ┌─────▼──────┐ ┌──────▼──────────┐
│ PLC / RTU │ │ Power Meter│ │ Serial Gateway │
│ (Server #1) │ │ (Server #2)│ │ (Server #3) │
│ Unit ID: 1 │ │ Unit ID: 1 │ │ Unit ID: 1-247 │
└──────────────┘ └────────────┘ │ ┌──┬──┬──┐ │
│ │ │ │ │ │
│ RS-485 serial │
│ bus (Modbus RTU)│
└──────────────────┘
▲ ▲
│ │
Works with most vendors' devices Unit ID routes through
No profiles, no handshake gateway to serial devices
The client polls each server on a schedule: “give me registers 40001-40010.” The server responds with raw values. There are no subscriptions and no events.
Request-Response Sequence
Client Server (Unit ID: 1)
│ │
│──── TCP SYN ──────────────────────────►│ Connection setup
│◄─── TCP SYN-ACK ─────────────────────│ (one-time)
│──── TCP ACK ──────────────────────────►│
│ │
│──── FC03: Read 10 regs at 40001 ─────►│ Poll #1
│◄─── Response: 20 bytes of data ───────│ ~2-5ms on Ethernet
│ │
│──── FC03: Read 10 regs at 40001 ─────►│ Poll #2 (1s later)
│◄─── Response: 20 bytes of data ───────│
│ │
│──── FC03: Read 10 regs at 40001 ─────►│ Poll #3
│ ... no response ... │ Device busy / offline
│ ... timeout (1-5s) ... │
│──── FC03: Read 10 regs at 40001 ─────►│ Retry
│◄─── Exception: 04 (Device Failure) ───│ Modbus error, not TCP error
│ │
The connection stays open between polls. Each poll is one request, one response. Many clients wait for each response before sending the next request. Modbus TCP also lets a client send several requests on one connection without waiting, each response matched to its request by Transaction ID (Implementation Guide V1.0b, §4.2.1.1) — serial Modbus does not.
Frame Structure
Request Frame (Client → Server):
┌────────────┬─────────────┬────────┬─────────┬──────────────┬──────────┬───────────┐
│ Transaction│ Protocol ID │ Length │ Unit ID │ Function Code│ Start Reg│ Reg Count │
│ ID (2B) │ (2B: 0x0000)│ (2B) │ (1B) │ (1B: 0x03) │ (2B) │ (2B) │
├────────────┴─────────────┴────────┴─────────┼──────────────┴──────────┴───────────┤
│ MBAP Header (7 bytes) │ PDU (5 bytes) │
└──────────────────────────────────────────────┴────────────────────────────────────┘
Response Frame (Server → Client):
┌────────────┬─────────────┬────────┬─────────┬──────────────┬───────────┬──────────┐
│ Transaction│ Protocol ID │ Length │ Unit ID │ Function Code│ Byte Count│ Register │
│ ID (2B) │ (2B: 0x0000)│ (2B) │ (1B) │ (1B: 0x03) │ (1B) │ Data ... │
├────────────┴─────────────┴────────┴─────────┼──────────────┴───────────┴──────────┤
│ MBAP Header (7 bytes) │ PDU (variable) │
└──────────────────────────────────────────────┴────────────────────────────────────┘
Transaction ID: matches request to response (critical when gateway multiplexes)
Protocol ID: always 0x0000 for Modbus (other values reserved)
Unit ID: routes through gateways to serial devices (1-247)
Security Exposure: The Compatibility-Security Tradeoff
Why it’s compatible:
[Any Client] ──── TCP 502 ────► [Any Server]
No app handshake No profiles to configure
No negotiation No certificates to manage
No authentication No version compatibility issues
Just open a socket and start reading registers.
This is why so many devices support it.
Why it’s exposed:
[Any Client] ──── TCP 502 ────► [Any Server]
✓ FC 01-04: Read coils, inputs, registers
⚠ FC 05: Write Single Coil ← flip a breaker output
⚠ FC 06: Write Single Register ← change a setpoint
⚠ FC 15: Write Multiple Coils ← flip many outputs at once
⚠ FC 16: Write Multiple Registers ← rewrite configuration
No authentication means no distinction between
"authorized SCADA system" and "attacker on the network."
The fix — defense in depth around the protocol:
┌──────────────┐ ┌──────────────┐ ┌─────────┐ ┌──────────────┐
│ Authorized │────►│ Firewall │────►│ SCADA │────►│ Modbus │
│ Client │ │ / ACL │ │ VLAN │ │ Server │
└──────────────┘ └──────────────┘ └─────────┘ └──────────────┘
IP allowlist FC filtering L2 isolation Write logging
Block FC 5/6/ No cross-VLAN Alert on
15/16 from access unexpected FC
unauthorized IPs
By design, Modbus has no security — the protocol trusts the network to enforce access. This means network architecture is the security model.
Cheat Sheet
| Parameter | Value |
|---|---|
| Default TCP port | 502 |
| Max registers per read | 125 (function code 03/04) |
| Max coils per read | 2000 (function code 01/02) |
| Byte order | Big-endian (but 32-bit word swap is vendor-dependent) |
| Max connections | Set by each device maker; the Modbus specification leaves it open |
| Typical poll interval | 500ms-2s (faster is possible but check device limits) |
| Unit ID | Behind a gateway: the serial slave address (1-247; 0 = serial broadcast). Direct TCP device: not significant — 0xFF recommended, 0 also accepted (Implementation Guide V1.0b, §4.4.1) |
Common function codes:
| Code | Name | Use |
|---|---|---|
| 01 | Read Coils | Read discrete outputs (on/off) |
| 02 | Read Discrete Inputs | Read discrete inputs (on/off) |
| 03 | Read Holding Registers | Read read/write registers (4xxxx) |
| 04 | Read Input Registers | Read read-only registers (3xxxx) |
| 05 | Write Single Coil | Set one discrete output |
| 06 | Write Single Register | Write one holding register |
| 15 | Write Multiple Coils | Set multiple discrete outputs |
| 16 | Write Multiple Registers | Write multiple holding registers |
Wireshark filters — one complete filter per line (Wireshark has no comment syntax); notes in the table below:
modbus
modbus && ip.addr == 10.1.1.100
modbus.func_code == 3
modbus.exception_code != 0
tcp.port == 502
| Filter | What it shows |
|---|---|
modbus |
All Modbus traffic |
modbus && ip.addr == 10.1.1.100 |
Modbus to or from one device |
modbus.func_code == 3 |
Read Holding Registers only |
modbus.exception_code != 0 |
Errors only |
tcp.port == 502 |
All traffic on the Modbus port |
Quick test with mbpoll:
# Read 10 holding registers starting at 40001 from device 10.1.1.100
mbpoll -a 1 -r 1 -c 10 -t 4:hex 10.1.1.100
# Write value 1 to register 40001 (e.g., close a breaker permissive)
mbpoll -a 1 -r 1 -t 4 10.1.1.100 -- 1
Best Practices
1. Isolate Modbus traffic on a dedicated VLAN with firewall rules at the zone boundary. Why: Modbus has zero built-in security. Network segmentation is the entire security model — there is no fallback inside the protocol. Every device on the same L2 segment can read every register and write every coil without authentication. If you don’t: An IT device, a compromised laptop, or a misconfigured switch port gets Layer 2 access to every Modbus server on the network. FC 5/6/15/16 write commands require no credential.
2. Restrict connections by source IP allowlist and disable write function codes for monitoring-only clients. Why: Any TCP connection a Modbus server accepts on port 502 has full access. There is no application-layer handshake, no login, no session token. If a client can reach the port, it can issue any function code — including writes that change physical outputs. If you don’t: A compromised HMI or rogue device writes arbitrary values to PLC coils. In a power system, that means opening breakers, changing setpoints, or disabling protective functions — without any audit trail from the protocol itself.
3. Set polling intervals based on measured device response time, not assumptions. Why: A serial Modbus line carries one transaction at a time: the client sends a request and waits for the response. Many TCP clients and gateways work the same way. If the poll interval is shorter than the device’s processing time (especially through serial gateways), requests queue up, timeouts fire, and the retry storm makes everything worse. If you don’t: A 100ms poll interval on a 250ms-response device creates cascading timeouts. The gateway queue overflows, data goes stale, and the SCADA system flags devices as offline when they’re only overwhelmed.
4. Build and version-control a register map for every device type before commissioning. Why: There is no standard Modbus register map. For example, register 40001 on an SEL-751 relay may mean something completely different from register 40001 on a Shark 250 meter. Vendor documentation is sometimes wrong, sometimes incomplete, and changes between firmware versions without notice. If you don’t: Polling the wrong register returns data that looks valid (a float in the expected range) but represents the wrong measurement. You’ll commission a monitoring system that reports incorrect values, and nobody catches it until an operator notices the readings don’t match the device display.
5. Tune response timeouts separately for each device class, accounting for gateway hops. Why: A PLC on Ethernet responds in 2-5ms. A serial meter behind a Modbus TCP-to-RTU gateway responds in about 30-150ms because of serial bus turnaround time and gateway queue processing. A single global timeout that works for PLCs will be too aggressive for serial devices; one that works for serial devices will be too slow to detect PLC failures. If you don’t: Serial devices behind gateways randomly time out during normal operation. Engineers increase the global timeout to compensate, which masks real failures on the Ethernet-connected devices — a dead PLC takes 5 seconds to detect instead of 500ms.
6. Log and alert on all write operations (FC 5, 6, 15, 16) from SCADA to field devices. Why: Write operations are the highest-consequence Modbus transactions — they change physical state (breaker position, setpoints, control modes). Since Modbus provides no authentication or audit trail, external logging is the only way to detect unauthorized or accidental writes. If you don’t: A misconfigured poll definition accidentally issues a write instead of a read. A value gets written to a coil or a register. The device changes state. Nobody notices until an operator sees a breaker in the wrong position — or, worse, until a fault event, when the protection logic doesn’t behave as expected.
7. Establish naming conventions, poll group tiers, and change control for register maps.
Why: Large SCADA systems accumulate hundreds of Modbus registers across dozens of devices. Without a naming convention, RTAC tag databases become unreadable within months. Without tiered poll groups, relay and breaker status competes for poll time with slow-changing environmental readings. Without change control, register map documents drift from the actual RTAC configuration — and the next engineer trusts the document, not the config.
If you don’t: Tags accumulate with inconsistent names (ATS1_kW, kW_ATS_1, ats1power). Poll schedules either over-poll everything (wasting bandwidth and gateway capacity) or under-poll relays and breakers (status goes stale between polls). Register map changes get made in the RTAC but not in the document, or vice versa, and the discrepancy surfaces during commissioning when the value at an address doesn’t match the document.
Adopt a register naming convention that encodes device type, instance, phase, and measurement — for example, PM_ATS1_PhA_kW for a power meter on ATS-1, Phase A, kilowatts. Set poll group intervals by device class: protection relays and breaker status at 1-second intervals, power meters and load readings at 5 seconds, environmental sensors (temperature, humidity) at 30 seconds. Maintain a version-controlled register map document with columns for Modbus address, data type, scaling factor, engineering units, RTAC tag name, and assigned poll group. Treat the register map and the RTAC configuration as a matched pair — any change to one requires an update to the other in the same revision, reviewed and approved before deployment.
Strengths, Weaknesses & When to Choose an Alternative
Strengths
Near-universal device support — the TCP/IP of industrial protocols. Most industrial devices made in the last 30 years speak Modbus. Power meters, protection relays, PLCs, UPS systems, generator controllers, building automation — Modbus is the lowest common denominator. When you need to integrate a device from any vendor into any SCADA system, Modbus is the protocol most likely to be available. This reach follows directly from the protocol’s simplicity. There is nothing to negotiate, no profiles to match, no capability exchange. Open a socket, read a register.
Simplicity as a feature — debug with standard tools in minutes. A Modbus transaction is one request, one response, with deterministic framing. There is no state machine beyond “connected or not.” A single Wireshark capture tells you everything: what was asked, what was answered, how long it took. Compare this to OPC UA (session establishment, security negotiation, subscription management) or DNP3 (application-layer fragmentation, unsolicited response state machine) — Modbus troubleshooting requires no specialized protocol knowledge beyond “function code + register address + value.”
Minimal resource requirements — runs on small devices. The protocol specification is 50 pages. A compliant implementation fits in kilobytes of flash. Devices with 8-bit microcontrollers and 32KB of RAM run Modbus servers. This matters in brownfield environments where existing devices cannot be replaced and no other protocol is available — Modbus is often the only option that fits the device’s hardware constraints.
Mature tooling ecosystem. Wireshark decodes Modbus natively. mbpoll, pymodbus, and ModRSsim2 are free and well-documented. SCADA platforms, gateways, and historians commonly ship a Modbus driver; “does it support Modbus?” is rarely a question. This ecosystem maturity takes driver availability off the risk list; the integration risk that remains is device-level: register maps, word order, and addressing (see Biggest Pitfalls).
Predictable failure modes — decades of field knowledge. Modbus has been deployed in production since 1979. Most failure modes are well documented, and the common ones (connection exhaustion, word swap confusion, register map mismatches, polling storms) have proven fixes. What remains is device-specific (see Edge Cases and Undocumented Behavior). The same cannot be said for newer protocols where the deployment base is smaller and the edge cases are still being discovered.
Weaknesses
Zero native security — the flip side of “no negotiation.” The same design choice that makes Modbus widely compatible makes it widely exposed. No authentication means any TCP connection to port 502 has full read and write access. No encryption means every register value and write command is visible in cleartext. No access control means a monitoring client and an attacker have identical capabilities. Security cannot be patched into classic Modbus TCP in place; the Modbus Organization’s answer, Modbus/TCP Security, is a separate TLS variant on port 802 that leaves port 502 unchanged (MB-TCP-Security v36, 2021, §5) — and every classic device stays as exposed as it was.
Poll-only architecture — latency is structural, not tunable. Modbus clients ask; servers answer. There is no mechanism for a device to report a change spontaneously. If a breaker trips between polls, the SCADA system learns about it at the next poll cycle — which could be 500ms to 2 seconds later. For monitoring, this is acceptable. For protection signaling and event reporting, Modbus polling is architecturally incapable of competing: DNP3 delivers event-driven unsolicited responses with millisecond timestamps, and GOOSE delivers multicast state changes within milliseconds (see the GOOSE page).
No semantic data model — registers are meaningless without documentation. Register 40001 is a 16-bit number. The protocol assigns no name, no unit, no data type, no quality indicator. Every vendor defines its own register map. Every device model has a different layout. Every firmware version can change the mapping. This means integration work scales linearly with device count — there is no “auto-discover” capability, no standard address space to browse. OPC UA’s self-describing object hierarchy and DNP3’s typed data points with quality flags both solve problems that Modbus does not even acknowledge.
Vendor-fragmented register maps — every device is a special case. For example, register 40001 on one relay might be Phase A Current (IEEE 754 float, two registers, big-endian). The same address on a power meter could be Volts A-N (32-bit signed integer, word-swapped). On a generator controller, it might be Engine RPM (16-bit unsigned, single register). There is no standard, no convention, no compatibility. Building and maintaining per-device register map documentation is a mandatory cost of every Modbus integration — and register maps rot as firmware versions change.
Connection ceiling — device resources are the bottleneck. Each Modbus device accepts only as many TCP connections as its maker allows, and the limit can be small. When the SCADA system, historian, HMI, and engineering workstation all need data from the same device, the connection slots fill. When they do, the device refuses the new connection or closes an existing one; the Modbus implementation guide recommends closing the oldest unused connection. This forces architectural choices (centralized gateway as single point of data aggregation) that add complexity and single points of failure.
When to Use
- Polling-based monitoring and metering where 500ms-2s data freshness is acceptable
- Brownfield integration with legacy devices that support no other protocol
- Simple point-to-point data acquisition between a gateway and field devices
- Low-bandwidth or resource-constrained environments where protocol overhead matters
- Multi-vendor environments where Modbus is the only common protocol across all devices
When NOT to Use
- Event-driven reporting where polling latency is unacceptable — use DNP3 unsolicited responses with Class 1/2/3 event buffers and millisecond timestamps
- Semantic data integration requiring self-describing object models — use OPC UA with typed address spaces, browse/discovery, and subscription-based monitoring
- Protection signaling or critical interlocking requiring deterministic delivery within a few milliseconds — use IEC 61850 GOOSE with multicast publish/subscribe
- Security-critical paths without compensating network controls — Modbus provides no authentication, encryption, or access control. Either add defense-in-depth (VLANs, firewalls, IDS) or choose a protocol with native security (OPC UA, DNP3 Secure Authentication, or Modbus/TCP Security where the devices support it)
- Hundreds of devices behind a single poller — the poll cycle stretches and device connection limits bite; partition polling across gateways (see Gateway cascading)
Common Misconception
“Modbus is insecure, so we should replace it with a secure protocol.”
This framing misunderstands where Modbus security actually lives. Modbus is transparent — it makes no attempt to restrict access because it was designed for physically isolated serial networks where the locked equipment room was the security boundary. Replacing Modbus with OPC UA or DNP3 Secure Authentication on a flat, unsegmented network does not make the system secure — it adds protocol-level authentication to an architecture that still allows Layer 2 access to every device. The correct approach is defense-in-depth: VLAN isolation, firewall ACLs, function code filtering, and write logging around Modbus. Network controls come first, and protocol security does not replace them. What the network cannot stop is a compromised host on an allowed path (see Best Practice 2). Where devices support Modbus/TCP Security or another authenticated protocol, use it to guard against unauthenticated access, impersonation, and tampering in transit; a compromised authorized client keeps its own permissions, so least-privilege write rights, endpoint protection, and independent control safeguards still address that risk. Conversely, a “secure” protocol deployed on a flat network is security theater.
Comparison Table
| Dimension | Modbus TCP | DNP3 | OPC UA |
|---|---|---|---|
| Data model | Numbered registers (no semantics) | Typed points (BI/AI/BO/AO) with quality flags and timestamps | Self-describing object hierarchy with browse and discovery |
| Security | None in classic Modbus TCP (port 502) — network-level only; Modbus/TCP Security (TLS) where supported | Secure Authentication v5 (HMAC challenge-response, thin adoption) | X.509 certs, AES-256, per-node ACL (strong model, weak practice) |
| Event support | None — polling only | Native: Class 1/2/3 event buffers with unsolicited reporting | Subscriptions with monitored items and deadband filtering |
| Reporting mode | Client polls, server responds | Both polling and unsolicited (event-driven) | Subscription-based (server pushes changes) |
| WAN suitability | Poor (polling over slow links wastes bandwidth) | Excellent (designed for serial, cellular, satellite) | Poor (LAN-first, stateful sessions, heavy handshake) |
| Implementation effort | Days | Weeks | Weeks to months |
| Wire overhead | Minimal (12-byte request) | Moderate (CRC, transport fragments, application headers) | Heavy (session + security negotiation + encoding) |
| Device support | Very wide (most industrial devices) | Strong in utilities, limited elsewhere | Growing (gateways widespread, native device support patchy) |
| Spec complexity | ~50 pages (+46 for the TCP implementation guide) | Several hundred pages (IEEE 1815) | Hundreds of pages across many parts (IEC 62541) |
| Role in Ziggurat’s Distributed Control Architecture (DCA) | Device polling and metering (field layer) | Northbound reporting to remote SCADA (WAN layer) | Aggregation layer: semantic gateway and historian integration, where a design calls for it |
Biggest Pitfalls
1. Connection exhaustion Each Modbus device accepts only as many TCP connections as its maker allows, and the limit can be small. If your SCADA, historian, HMI, and engineering workstation all connect directly, you can hit it. The device then refuses the new connection or closes an existing one. Prevention: Route Modbus through a gateway (RTAC) so each device carries one connection and the gateway redistributes to consumers. Where the data must survive a gateway failure, use a redundant pair (see Redundant polling), which takes two connections per device.
2. Word swap confusion Modbus registers are 16-bit. A 32-bit float spans two registers. The spec sets big-endian within each 16-bit register and says nothing about 32-bit values; many devices put the low word first. Get the word order wrong and you’ll read a power meter value of 2.004 instead of 480.5 (0x43F0 4000 read as 0x4000 43F0). Prevention: Always check the device manual for word order. Test with a known value during commissioning. Document the swap setting per device in your register map.
3. Polling too fast Setting a 100ms poll interval on a device with a 250ms processing time causes timeouts and retries. The retry storm makes things worse — the device falls further behind. Prevention: Match the poll interval to the device’s measured response time. Start at 1000ms and reduce only if the device handles it. Monitor timeout counts.
4. Register map assumptions Register 40001 on one relay is not the same as register 40001 on a different vendor’s meter. Every vendor defines its own register map. Copying a poll configuration from one device type to another produces garbage data. Prevention: Always build register maps from the device manual. Never assume register addresses are portable between vendors.
5. No error handling for exception responses Modbus exception responses (error codes) are valid protocol responses — not TCP errors. A SCADA system that checks only for TCP timeouts will miss Modbus-level errors like “illegal data address” (code 02) or “device busy” (code 06). Prevention: Configure your gateway to log Modbus exception codes. Alert on persistent exceptions — they indicate misconfiguration, not transient errors.
Field Tips & Tools
Wireshark Setup for Modbus TCP Commissioning
Step 1 — Capture filter (reduce capture size):
tcp port 502
Step 2 — Display coloring (View → Coloring Rules; Wireshark applies the first rule that matches, so place these three rules above the default rules, Red first):
- Red:
modbus.exception_code != 0(errors) - Yellow:
tcp.analysis.retransmission(TCP retransmissions — lost segments; check the network and the device) - Green:
modbus && !modbus.exception_code(normal read/write)
Step 3 — Custom columns (right-click column header → Column Preferences):
- Unit ID:
mbtcp.unit_id - Function:
modbus.func_code - Register:
modbus.reference_num - Exception:
modbus.exception_code
Step 4 — Statistics (Statistics → Conversations → TCP): Check how many connections to each device IP are open at the same time against the limit in that device’s manual; a count at or near the limit signals a connection exhaustion risk. Take the count from the device’s own connection diagnostics, or from a capture that includes connection setup and teardown. The Conversations total alone counts connections over the whole capture, not concurrent ones, so use it only as supporting evidence.
Common Commissioning Sequence
- Verify IP connectivity:
ping <device-ip>— confirm network layer works - Verify port open:
nc -zv <device-ip> 502— confirm Modbus port is listening - Read a known register:
mbpoll -a <unit-id> -r 1 -c 1 -t 4 <device-ip>— confirm protocol works - Read the full register map: Poll all documented registers, compare against device front panel readings
- Write test (if applicable): Write a non-critical permissive, verify device state change
- Load test: Set target poll rate, run for 10 minutes, check for timeouts or exceptions
- Point-to-point checkout: Confirm each point end to end at the SCADA or EPMS display (value, scaling, units), exercise each alarm limit, and record the result per point
- Document: Capture register map, connection settings, poll rates, and word swap settings per device
Deep Dive
Protocol Stack
Modbus TCP operates at the Application layer over a standard TCP/IP stack. There is no session or presentation layer — the protocol is a direct mapping of Modbus application messages onto TCP.
┌─────────────────────────┐
│ Modbus Application │ Function codes + register data
│ (PDU: FC + Data) │
├─────────────────────────┤
│ MBAP Header │ Transaction ID, Protocol ID, Length, Unit ID
│ (7 bytes) │ Replaces RTU's CRC — TCP handles integrity
├─────────────────────────┤
│ TCP │ Port 502, connection-oriented, reliable delivery
├─────────────────────────┤
│ IP │ Standard IPv4 addressing
├─────────────────────────┤
│ Ethernet │ Standard IEEE 802.3
└─────────────────────────┘
The MBAP (Modbus Application Protocol) header replaces Modbus RTU’s CRC and slave address. TCP guarantees delivery integrity, so the CRC is unnecessary. The Unit ID field replaces the RTU slave address — it routes requests through gateways to serial devices behind them.
Key difference from RTU: In Modbus RTU, the slave address identifies a physical device on a serial bus. In Modbus TCP, the IP address identifies the TCP endpoint, and the Unit ID routes within that endpoint. A single gateway at one IP can reach each serial device behind it by its Unit ID.
Frame Format
MBAP Header (7 bytes):
| Byte offset | Field | Size | Value |
|---|---|---|---|
| 0-1 | Transaction ID | 2 bytes | Client-assigned, echoed in response. Matches requests to responses when gateway multiplexes. |
| 2-3 | Protocol ID | 2 bytes | Always 0x0000 for Modbus. Other values are reserved — reject any frame with a non-zero Protocol ID. |
| 4-5 | Length | 2 bytes | Number of bytes following (Unit ID + PDU). Max 254 for standard frames. |
| 6 | Unit ID | 1 byte | 0xFF (or 0) for a device connected directly to TCP/IP; 1-247 = slave address behind a gateway (0 = serial-line broadcast). |
PDU (Protocol Data Unit):
| Field | Size | Description |
|---|---|---|
| Function Code | 1 byte | The operation: read (01-04), write (05-06, 15-16), diagnostic (43; 07-08 serial line only) |
| Data | 0-252 bytes | Request parameters or response payload. Max 252 bytes, giving 253 total PDU bytes. |
Function Code Reference:
| Code | Name | Direction | Registers | Risk Level |
|---|---|---|---|---|
| 01 | Read Coils | Read | Discrete outputs | Low — read-only |
| 02 | Read Discrete Inputs | Read | Discrete inputs | Low — read-only |
| 03 | Read Holding Registers | Read | R/W registers (4xxxx) | Low — read-only |
| 04 | Read Input Registers | Read | R/O registers (3xxxx) | Low — read-only |
| 05 | Write Single Coil | Write | One discrete output | High — changes physical state |
| 06 | Write Single Register | Write | One holding register | High — changes setpoints |
| 15 | Write Multiple Coils | Write | Multiple discrete outputs | Critical — bulk state change |
| 16 | Write Multiple Registers | Write | Multiple holding registers | Critical — bulk config change |
| 23 | Read/Write Multiple | Both | Holding registers | Critical — atomic read + write |
| 43 | Read Device ID | Read | Device metadata | Medium — reconnaissance value |
Exception Responses: When a server cannot fulfill a request, it returns the function code with bit 7 set (FC + 0x80) followed by a 1-byte exception code:
| Code | Name | Common Cause |
|---|---|---|
| 01 | Illegal Function | Server doesn’t support this FC |
| 02 | Illegal Data Address | Register address out of range |
| 03 | Illegal Data Value | A request field the server cannot accept (e.g. quantity out of range, wrong length, an FC05 coil value other than 0x0000 or 0xFF00) — not a check of a register value against the application’s own limits |
| 04 | Server Device Failure | Internal device error — not a Modbus problem |
| 06 | Server Device Busy | Device processing a long operation — retry later |
Timing and Performance
Latency budget for a single Modbus TCP transaction:
| Stage | Ethernet-direct | Through serial gateway |
|---|---|---|
| TCP round-trip | 1-2ms | 1-2ms |
| Server processing | 1-3ms | 1-3ms (gateway) |
| Serial bus turnaround | — | 20-100ms |
| Serial device response | — | 5-50ms |
| Total | 2-5ms | 27-155ms |
Throughput calculation: Modbus TCP can read a maximum of 125 holding registers (250 bytes) per FC03 request. Reading 125 registers per poll at a 1-second poll interval gives 125 register values per second. To poll 1,000 registers from a single device, you need 8 requests per cycle — at 2-5ms each on Ethernet, 16-40ms per poll cycle when the client sends one request at a time.
Connection concurrency: The Modbus specification sets no number of TCP connections; it leaves the count to each implementation and its device capacities (Modbus Messaging on TCP/IP Implementation Guide V1.0b, §4.2.1.1). Limits therefore vary by device: ABB’s REC615 and RER615 relays accept five client connections in total, shared with IEC 61850 MMS and every other protocol (ABB 1MRS758758 Rev. A, §3.1.1), while a Moxa MGate MB3180 gateway accepts 16 simultaneous Modbus TCP masters (MGate MB3180/MB3280/MB3480 datasheet). Check the limit in each device’s manual and design to the smallest one.
Determinism: The protocol sets no latency bound. Modbus TCP inherits TCP’s retransmission behavior — a single dropped packet can spike latency from 5ms to 200ms+. For time-critical applications (protection, interlocking), this non-determinism disqualifies Modbus. Use IEC 61850 GOOSE for deterministic messaging.
Security Model
Built-in security: none in classic Modbus TCP (port 502). Modbus was designed in 1979 for serial connections between local devices. Security was the locked door on the equipment room. TCP connectivity extended the protocol’s reach without extending its trust model. The Modbus Organization has since published Modbus/TCP Security (TLS on port 802, certificate authentication, role-based authorization; MB-TCP-Security v36, 2021), but it is a separate variant: a classic device gains nothing from it.
What’s missing:
- Authentication: No mechanism to verify client identity. Any TCP connection to port 502 has full access.
- Authorization: No function code restrictions. A monitoring-only client has the same write access as the SCADA master.
- Encryption: All data travels in cleartext. Register values, device metadata, and write commands are visible to any network observer.
- Integrity: TCP checksums protect against corruption, but not against deliberate modification. A man-in-the-middle can alter register values or inject write commands.
- Audit trail: No session logging, no transaction history, no event records in the protocol.
Attack surface:
- Reconnaissance: FC 43 (Read Device Identification) reveals vendor, product code, firmware version — enough to identify known vulnerabilities. FC 01-04 reads expose all process data without authentication.
- Write attacks: FC 05/06/15/16 can change physical outputs. In a power system: open breakers, change generator setpoints, disable protection functions. No credential is required.
- Denial of service: An attacker can flood a device with requests to exhaust its connection slots (a small connection limit fills quickly) or send rapid FC writes to keep the device continuously processing state changes.
- Internet exposure: Shodan regularly indexes Modbus devices on port 502. Devices behind misconfigured firewalls or VPN gateways are discoverable and directly addressable.
Regulatory context:
- NERC CIP-005 (Electronic Security Perimeters): Modbus devices that influence bulk electric system reliability fall in scope. Network segmentation, access control, and monitoring are mandatory — the protocol provides none of these natively.
- NERC CIP-007 (System Security Management): Requires security patch management, malicious software prevention, and port/service management, among other controls. Most Modbus devices cannot be patched or run antivirus — defense must be network-based.
- IEC 62351-5: Defines security for IEC 60870-5-104 (a TCP-based telecontrol protocol) but does not apply to Modbus directly. There is no IEC security standard for Modbus TCP; the Modbus Organization’s own Modbus/TCP Security specification (TLS, port 802) is the protocol-level option.
- IEC 62443: The zones-and-conduits model for industrial networks — the frame for the segmentation, conduit firewalls, and monitoring in the table below. It governs the architecture around Modbus, not the protocol itself.
Defense-in-depth architecture:
| Layer | Control | Purpose |
|---|---|---|
| Network | VLAN isolation | Prevent unauthorized L2 access to Modbus segment |
| Network | Firewall ACLs | Restrict source IPs to known SCADA systems |
| Application | DPI / FC filtering | Block write function codes from unauthorized sources |
| Application | Modbus-aware IDS | Detect anomalous traffic: unexpected FCs, new source IPs, abnormal register ranges |
| Monitoring | Write logging | Record all FC 5/6/15/16 transactions with timestamp, source, target, values |
| Monitoring | Baseline comparison | Alert when register read patterns deviate from established polling schedule |
Example firewall and IDS configurations:
- Source-IP ACL: Restrict TCP/502 access to known SCADA master IPs only. Configure RTAC host-allow lists to refuse connections from any source not on the allowlist. At the firewall, drop — do not reject — traffic from unknown sources to avoid leaking network topology information.
- Function code filtering: Deploy a DPI-capable firewall that inspects Modbus application-layer payloads. Block write function codes (FC 5, 6, 15, 16) from any source other than the designated SCADA master. Read-only sources (historians, dashboards, trending workstations) should never be able to issue writes, even accidentally.
- Anomaly baseline: Capture 24 hours of normal Modbus traffic during steady-state operation to establish a behavioral baseline — expected source IPs, function codes in use, register address ranges per device. Configure IDS alerts for new source IPs appearing on the Modbus VLAN, function codes not seen during baselining, and register addresses outside the documented map range.
Interoperability
The register map problem: Modbus defines a protocol, not a data model. The specification says registers exist at addresses 0-65535. It says nothing about what those registers mean. Every vendor defines its own register map — address assignments, data types, scaling factors, byte ordering.
In practice, this means:
- Register 40001 on a protection relay might be “Phase A Current” (IEEE 754 float, two registers, big-endian).
- The same address on a power meter could be “Volts A-N” (32-bit signed integer, word-swapped).
- On a generator controller, it might be “Engine RPM” (16-bit unsigned integer, single register).
There is no way to auto-discover register meanings. You must read the vendor manual for each device model and firmware version.
Vendor-specific function codes: The Modbus spec sets aside FC 65-72 and FC 100-110 as user-defined function codes and does not guarantee they are unique. These are undocumented outside vendor literature and may conflict between manufacturers. Never use vendor-specific FCs in gateway configurations that serve multiple device types.
Byte and word order: The Modbus specification defines big-endian byte order for 16-bit registers. For 32-bit values spanning two registers, four orderings exist in the wild:
| Order | Bytes | Common In |
|---|---|---|
| Big-endian (AB CD) | MSB first, high register first | Most common (high word first); the spec defines no 32-bit order |
| Little-endian (DC BA) | LSB first, low register first | Some devices |
| Mid-big (CD AB) | MSB first, low register first | Some devices |
| Mid-little (BA DC) | LSB first, high register first | Rare, but it exists |
The only reliable approach: read a known value (e.g., a voltage that the device display shows as 480.0), try all four interpretations, and document which one matches. Do this for every device type. Do not assume order is consistent across register types on the same device — some devices use different ordering for floats and integers.
Conformance testing: The Modbus Organization maintains a conformance test suite, but testing is voluntary and rarely performed. In practice, “Modbus TCP support” on a device datasheet means “we implemented the function codes we cared about.” Expect gaps: some devices don’t support FC 23 (read/write multiple), some don’t implement exception codes correctly, and some crash on broadcast writes (Unit ID 0).
Advanced Configuration
Connection pooling: Production SCADA systems maintain persistent TCP connections to each Modbus server. Connecting per poll cycle adds a handshake round trip (1-2ms on a LAN) to every transaction and wastes connection slots. Best practice: open once, poll forever, reconnect only on failure. Implement a watchdog that detects stale connections (no response to N consecutive polls) and reconnects.
Gateway cascading: A common architecture in large systems:
SCADA ──TCP──► Gateway (RTAC) ──TCP──► Sub-gateway (Moxa) ──RS-485──► Meters
Each gateway hop adds latency and a potential failure point. Unit ID routing must be planned end-to-end: the RTAC addresses the Moxa by IP; the Moxa addresses the serial meter by Unit ID. If two serial buses behind the same Moxa have devices with the same Unit ID, you need per-port Unit ID mapping — not all gateways support this.
Serial bridging timing: When a Modbus TCP request transits a serial gateway, the gateway must:
- Receive the full TCP frame
- Strip the MBAP header, add RTU CRC
- Wait for the serial bus to be idle (inter-frame gap: 3.5 character times at the baud rate)
- Transmit the RTU frame at the serial baud rate (9600/19200/38400 bps)
- Wait for the serial response
- Strip the CRC, add MBAP header, respond on TCP
At 9600 bps, transmitting a 20-byte write request takes ~23ms (11-bit characters). The inter-frame gap at 9600 bps is ~4ms. Total gateway overhead before the device even starts processing: ~27ms. This is why serial-bridged devices need much longer timeouts than Ethernet-direct devices.
Redundant polling: For critical monitoring, deploy two SCADA clients polling the same devices. Strategies:
- Active-standby: Primary polls; standby monitors primary heartbeat and takes over on failure. Simple but adds switchover delay.
- Active-active: Both poll; consumers take the freshest value. Higher connection usage (2 slots per device) but zero switchover delay.
Edge Cases and Undocumented Behavior
Transaction ID wraparound: The Transaction ID is a 16-bit unsigned integer (0-65535). A client sending 100 requests per second wraps in ~11 minutes. Most implementations handle this correctly, but some older gateways match responses by Transaction ID without accounting for wraparound — a response to Transaction ID 0 after wraparound may match a stale outstanding request from 65536 transactions ago.
Gateway Unit ID conflicts: Two serial buses behind the same gateway, each with a device at Unit ID 1. The Modbus spec provides no mechanism for disambiguation — the gateway must support per-port Unit ID mapping (e.g., “port 1 Unit ID 1 → remapped Unit ID 1, port 2 Unit ID 1 → remapped Unit ID 101”). Not all gateways support this. Some silently route to the first matching port.
TCP keepalive vs. Modbus polling: Some Modbus servers close idle TCP connections after 30-60 seconds. If the poll interval is longer than the server’s idle timeout, every poll cycle starts with a connection setup. Solutions: (1) reduce the poll interval below the idle timeout, (2) enable TCP keepalive at the OS level (typically 2-hour default — too slow, must be tuned), or (3) send a lightweight FC 03 read (1 register) as a keepalive between real poll cycles.
Broadcast writes (Unit ID 0): On a serial line, address 0 is the broadcast address: a write goes to every device on the bus and none replies. Through a gateway, Unit ID 0 becomes that serial broadcast if the gateway passes it on; a device connected directly to TCP/IP treats 0 as addressed to itself (Implementation Guide V1.0b, §4.4.1). In practice, some devices ignore broadcasts and some crash or reboot on a broadcast write. Never use Unit ID 0 in production.
Half-open connections: If a Modbus TCP client crashes without closing its TCP connection (power loss, OS crash, network partition), the server holds the connection slot open until its own TCP timeout expires. On a device with few connection slots and a long TCP timeout, a client crash can lock out other clients until the timeout expires. Mitigation: configure aggressive TCP keepalive (10-30 second interval) on the server side to detect dead connections faster.
Register addressing: 0-based vs. 1-based: The Modbus protocol uses 0-based register addressing internally (PDU address 0 = the first register). But most documentation and tools use 1-based addressing (register “40001” = PDU address 0 for holding registers). Off-by-one errors between 0-based and 1-based addressing are among the most common Modbus integration bugs. When debugging unexpected values, always check whether the tool and the device documentation agree on addressing convention.
Gateway-specific edge cases for commissioning:
- Moxa NPort serial gateways: The default TCP alive-check interval is 7 minutes — far too long when firewalls or upstream network equipment enforce idle-connection timeouts of 60-120 seconds. Connections drop silently, and the RTAC sees intermittent timeouts with no pattern. Set the Moxa TCP alive-check to 30 seconds to stay well inside typical firewall idle thresholds.
- SEL RTAC as Modbus gateway: When the RTAC bridges Modbus TCP to serial devices, the per-port timeout must account for the slowest device on that serial bus. Set the upstream SCADA timeout to the RTAC port timeout plus a 500ms buffer — otherwise the SCADA master times out before the RTAC finishes waiting for the serial response, and the late reply gets discarded or mismatched.
- Register addressing traps: Most vendor documentation uses 1-based register numbering (register 40001), but the Modbus protocol PDU uses 0-based addressing (address 0x0000 for the first holding register). Off-by-one errors between documentation and protocol are among the most common commissioning bugs. Always confirm the actual register address with a Wireshark capture before trusting the documentation.
Need Help With Modbus TCP?
Our team has field-proven experience with this protocol.
Let's discuss your project requirements.