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 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 byte order is the default — but many devices swap words for 32-bit values, creating endless confusion
- No built-in security — authentication, encryption, and access control must come from the network layer
- Polling-only — the master asks, the slave answers. No unsolicited reporting, no event-driven updates
- Connection management matters — each TCP connection consumes a slot on the device. Too many connections = device stops responding
- Register maps are vendor-specific — '40001' on a SEL relay means something completely different than '40001' on a Shark meter
- Modbus TCP is just Modbus RTU wrapped in TCP — 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 TCP is the most common communication protocol in industrial automation. If your data center has 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” — and the device responds with register values. The gateway aggregates this data for the operator HMI or historian.
What a prime contractor should care about: Modbus is reliable and universal, 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: If they propose Modbus TCP for critical interlocking or load shedding automation, they don’t understand the protocol hierarchy. Modbus is for the monitoring layer, not the control-critical path.
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 ANY vendor's device Unit ID routes through
No profiles, no configuration gateway to serial devices
The client polls each server on a schedule: “give me registers 40001-40010.” The server responds with raw values. No subscription, no events — just ask and answer.
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. No pipelining — the client waits for a response before sending the next request.
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 works with everything:
[Any Client] ──── TCP 502 ────► [Any Server]
No 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 every device supports 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
Modbus has no security by design — 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 | Typically 4-8 per device (vendor-specific) |
| Typical poll rate | 500ms-2s (faster is possible but check device limits) |
| Unit ID range | 1-247 (0 = broadcast, 248-255 reserved) |
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:
modbus # All Modbus traffic
modbus && ip.addr == 10.1.1.100 # Modbus to/from specific device
modbus.func_code == 3 # Read Holding Registers only
modbus.exception_code != 0 # Errors only
tcp.port == 502 # All traffic on 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: Modbus servers accept any TCP connection to port 502. There is no 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: Each Modbus transaction is serial — the client sends a request and waits for a response before the next request. 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 rate 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 actually just 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 a SEL-751 relay may be a completely different value than 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 50-200ms 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 timeout 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 register. The device changes state. Nobody notices until an operator sees a breaker in the wrong position — or worse, during 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, protection-critical data competes for bandwidth 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 protection devices (missing transient events). Register map changes get made in the RTAC but not the document — or vice versa — and the discrepancy surfaces during commissioning when a value doesn’t match the expected address.
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. Size poll groups 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
Universal device support — the TCP/IP of industrial protocols. Virtually every industrial device manufactured in the last 30 years speaks 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 you can count on being available. This universality is not an accident — it’s a direct consequence of 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 any tool 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 anything. 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, well-documented, and actively maintained. Every SCADA platform, every gateway, every historian has a Modbus driver. There is no “does it support Modbus?” question — it does. This ecosystem maturity reduces protocol-layer integration risk to near zero.
Predictable failure modes — decades of field knowledge. Modbus has been deployed in production since 1979. Every failure mode is documented, every edge case is known, every workaround is proven. Connection exhaustion, word swap confusion, register map mismatches, polling storms — these are understood problems with understood solutions. Few surprises remain in Modbus. 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 universally compatible makes it universally 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. This is not a bug to be patched — it is a fundamental design property from 1979 that cannot be changed without breaking backward compatibility with every existing device.
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 coordination where DNP3 delivers event-driven unsolicited responses with millisecond timestamps, or GOOSE delivers multicast state changes in under 4ms, Modbus polling is architecturally incapable of competing.
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 their 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 snowflake. 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. Most Modbus devices support 4-8 simultaneous TCP connections. When SCADA, historian, HMI, and engineering workstation all need data from the same device, the connection slots fill. The device silently drops new connections or, worse, drops existing ones. 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 sub-10ms deterministic delivery — 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)
- Large-scale deployments with hundreds of devices — polling does not scale gracefully, and connection limits create bottlenecks
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. The protocol doesn’t need to be secure if the network architecture enforces access control. 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 — network-level only | 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 | Ubiquitous (virtually every industrial device) | Strong in utilities, limited elsewhere | Growing (gateways widespread, native device support patchy) |
| Spec complexity | ~50 pages | ~800 pages (IEEE 1815) | ~1,200+ pages (IEC 62541, 14 parts) |
| Typical DCA role | Device polling and metering (field layer) | Northbound reporting to remote SCADA (WAN layer) | Semantic gateway and historian integration (aggregation layer) |
Biggest Pitfalls
1. Connection exhaustion Most Modbus devices support 4-8 simultaneous TCP connections. If your SCADA, historian, HMI, and engineering workstation all connect directly, you’ll hit the limit. The device stops accepting new connections — and may drop existing ones. Prevention: Route all Modbus through a single gateway (RTAC). One connection to the device, the gateway redistributes to consumers.
2. Word swap confusion Modbus registers are 16-bit. A 32-bit float spans two registers. The spec says big-endian, but many devices swap the two 16-bit words. You’ll read a power meter value of 3,145,728.0 instead of 480.0. 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 rate 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 poll rate to the device’s documented response time. Start at 1000ms, 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 their 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 only checks for TCP timeout 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 (Edit → Coloring Rules):
- Green:
modbus.func_code < 7(normal read/write) - Red:
modbus.exception_code != 0(errors) - Yellow:
tcp.analysis.retransmission(retries — symptom of device overload)
Step 3 — Custom columns (right-click column header → Column Preferences):
- Unit ID:
modbus.unit_id - Function:
modbus.func_code - Register:
modbus.reference_num - Exception:
modbus.exception_code
Step 4 — Statistics (Statistics → Conversations → TCP): Check connection count per device IP. More than 4-5 connections to the same device = connection exhaustion risk.
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
- 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 bridge to 247 serial devices via 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 | 0 = broadcast, 1-247 = device address, 248-255 = reserved. |
PDU (Protocol Data Unit):
| Field | Size | Description |
|---|---|---|
| Function Code | 1 byte | The operation: read (01-04), write (05-06, 15-16), diagnostic (07-08, 43) |
| 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 | Value out of device’s accepted range |
| 04 | Server Device Failure | Internal device error — not a Modbus problem |
| 06 | Server Busy | Device processing a long operation — retry later |
Timing and Performance
Latency budget for a single Modbus TCP transaction:
| Segment | 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. At a 1-second poll interval reading 125 registers per poll, that’s 125 register values per second. To poll 1,000 registers from a single device, you need 8 requests per cycle — at 5ms each on Ethernet, that’s 40ms minimum per poll cycle.
Connection concurrency: Most field devices support 1-8 simultaneous TCP connections. The Modbus specification doesn’t define a minimum — vendors choose based on device resources. Common limits:
| Device Class | Typical Max Connections |
|---|---|
| Power meters (Shark, ION) | 4-5 |
| Protection relays (SEL, ABB) | 5-8 |
| PLCs (Allen-Bradley, Siemens) | 8-16 |
| Serial gateways (RTAC, Moxa) | 16-32 |
Determinism: None. 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. This is not an oversight or a “future enhancement” — it is the defining characteristic of the protocol. Modbus was designed in 1979 for serial connections inside a single cabinet. Security was the locked door on the equipment room. TCP connectivity extended the protocol’s reach without extending its trust model.
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: Flood a device with requests to exhaust its connection slots (4-8 connections 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 (which shares Modbus’s TCP-based polling model) but does not apply to Modbus directly. There is no IEC security standard for Modbus TCP.
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 reject connections from any source not explicitly whitelisted. 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 their 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 reserves FC 65-72 and FC 100-110 for vendor-defined functions. 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 | Modbus spec default |
| Little-endian (DC BA) | LSB first, low register first | Some Allen-Bradley devices |
| Mid-big (CD AB) | MSB first, low register first | Many power meters |
| 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 3-way handshake latency (~3ms) 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 request takes ~21ms. The inter-frame gap at 9600 bps is ~4ms. Total gateway overhead before the device even starts processing: ~25ms. 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 polling 100 registers 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): The Modbus spec defines Unit ID 0 as broadcast — the request goes to all devices on the segment. In practice: some devices respond to broadcast reads (violating the spec — broadcasts should be write-only), some ignore broadcasts entirely, and some crash or reboot on receiving 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 devices with 4 connection slots and 5-minute TCP timeouts, a client crash can lock out other clients for 5 minutes. 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 the single most common Modbus integration bug. 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: Vendor documentation almost always 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 the most common commissioning bug. 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.