Skip to main content

Modbus TCP Server

This page documents the server route (MODBUS_SERVER)—the broker hosts a Modbus TCP server and exposes MQTT topics as register and coil addresses. External masters connect to the broker over TCP. To connect the broker to a remote Modbus device instead, see Modbus TCP Client.
The MODBUS_SERVER route makes your broker act as a Modbus TCP server (slave). External masters—SCADA, HMIs, historians, PLCs—connect to the broker and read or write a declared register/coil address space that bridges bidirectionally to MQTT topics. Use MODBUS_TCP when you need to poll a device. Use MODBUS_SERVER when OT systems need to read and write values that live on your MQTT bus—no separate gateway required.
Like a shop counter with a price list. MQTT updates what’s on the shelf (register values), and customers (Modbus masters) read the tags—or, for writable addresses, change items that publish back to MQTT.

When to Use This Route

  • SCADA / HMI integration — Let plant-floor tools poll broker data over standard Modbus without custom drivers.
  • OT/IT bridge — Expose IoT sensor streams to masters that only speak Modbus.
  • Bidirectional control — Writable tags let masters adjust setpoints; the broker publishes the new value to MQTT on a cadence you choose.
  • Client-only field devices — Integrate master-only equipment into MQTT with no middleware between the broker and the bus.

The sections below cover data flow, configuration, persistence, and production patterns.

Quick Start

Map an MQTT topic to a holding register masters can poll:
DEFINE ROUTE TempServer WITH TYPE MODBUS_SERVER
    ADD MAPPING Sensors
        ADD TAG Temperature
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "plant/line1/temperature"
Publish 23.5 to plant/line1/temperature → registers 100–101 hold the FLOAT; a master reading them gets 23.5.

How Data Flows

Sync is hybridWITH EVERY controls outbound latency:
DirectionBehavior
MQTT → register (inbound)A publish on a tag’s SOURCE_TOPIC updates the backing register or coil immediately. Works for every address type, including read-only INPUT_REGISTER and DISCRETE_INPUT. Retained MQTT values are seeded at startup.
register → MQTT (outbound)When a master writes a WRITABLE HOLDING_REGISTER or COIL, the broker detects the change on the mapping’s WITH EVERY cadence and publishes the decoded value to the tag’s DESTINATION_TOPIC (rounded per DECIMAL_PLACES, retained). A master write surfaces on MQTT within one cadence interval.
A master write never loops back into the register from MQTT, and an MQTT-driven change is never re-published to DESTINATION_TOPIC (direction guard). On server routes, route status Connected means the endpoint is listening—not that a remote session is established.

Server Configuration

All transport settings live in the MODBUS_SERVER_CONFIG block. A valid route needs only WITH TYPE MODBUS_SERVER plus a mapping with one tag—it listens on 0.0.0.0:502 as unit 1 by default.
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH HOST "0.0.0.0"
        WITH PORT 502
        WITH UNIT_ID 1
        WITH MAX_CONNECTIONS 64

Connection parameters

HOST
string
default:"0.0.0.0"
Bind interface to listen on (0.0.0.0 = all interfaces). IP is accepted as an alias.
PORT
integer
default:"502"
TCP listen port. Each server route claims its port exclusively—a second route on the same port is refused.
MAX_CONNECTIONS
integer
default:"64"
Best-effort soft cap on concurrent master connections. See Connection-limit behavior.
UNIT_ID
integer
default:"1"
Modbus unit ID the broker answers as (1–247). SLAVE_ID is accepted as an alias.
PERSISTENCE
boolean
default:"false"
Persist served values across broker restarts. See Data persistence & recovery.
ONE_BASED_ADDRESSES
boolean
default:"false"
Treat plain numeric tag addresses as one-based (address 1 = PDU register 0). Modicon-style addresses (400001+, 300001+, 100001+) are always decoded regardless of this flag. See Address notation.
Modbus has no native authentication—there is no credential configuration. Bind to a specific interface and restrict access at the network layer; MQTT-side access is governed by HUB topic permissions. Use GET ENV / GET SECRET for environment-derived values (for example WITH PORT GET ENV "MODBUS_PORT").

Tag and Mapping Configuration

Each exposed register or coil is declared as an ADD TAG. Only declared addresses are served—reads or writes to undeclared addresses return a Modbus Illegal Data Address exception.
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH HOST "0.0.0.0"
        WITH PORT 502
        WITH UNIT_ID 1
        WITH MAX_CONNECTIONS 64
    ADD MAPPING ServerTags
        WITH EVERY 500 MILLISECONDS
        WITH SOURCE_TOPIC "modbus/server"
        ADD TAG Setpoint
            WITH ADDRESS "200"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "modbus/server/setpoint"
            WITH WRITABLE "true"
            WITH DESTINATION_TOPIC "modbus/server/setpoint/set"
            WITH BYTE_ORDER "BIGENDIAN"
            WITH WORD_ORDER "BIGENDIAN"
            WITH DECIMAL_PLACES 2
            WITH DESCRIPTION "Operator setpoint"

Server vs. client semantics

SettingClient (MODBUS_TCP)Server (MODBUS_SERVER)
RoleMaster—dials out to a deviceSlave—listens for masters
HOST / IPDevice IP to dialBind interface to listen on
SOURCE_TOPICRegister value → MQTT (read)MQTT → register (write into address space)
DESTINATION_TOPICMQTT → device writeRegister → MQTT when a master writes
EVERYPoll intervalOutbound sync cadence (master write → MQTT)
TransportTCP / Serial (RTU)TCP

Modbus address banks

ADDRESS_TYPEBankElementMaster accessFunction codes
HOLDING_REGISTERHolding registers16-bit wordread / write3, 6, 16
INPUT_REGISTERInput registers16-bit wordread-only4
COILCoils1 bitread / write1, 5, 15
DISCRETE_INPUTDiscrete inputs1 bitread-only2

Address notation

Tag ADDRESS values are converted to the PDU offset the broker exposes on the wire. External Modbus masters always use zero-based PDU addresses in their read/write requests—the ONE_BASED_ADDRESSES flag only affects how you label tags in LoT, not how masters address the server. Three styles are supported (same rules as the Modbus TCP Client route):
StyleConfigTag address for PDU 0Tag address for PDU 99
Zero-based plain (default)ONE_BASED_ADDRESSES false"0""99"
One-based plainONE_BASED_ADDRESSES true"1""100"
Modicon holding registereither"400001""400100"
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH PORT 502
        WITH UNIT_ID 1
        WITH ONE_BASED_ADDRESSES true
    ADD MAPPING ServerTags
        WITH SOURCE_TOPIC "plant/modbus"
        ADD TAG Temperature
            WITH ADDRESS "1"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
Modicon-style addresses are decoded the same way regardless of ONE_BASED_ADDRESSES. The flag only affects plain numeric addresses.

Tag parameters

ADDRESS
string
required
Register or coil address. Plain numeric (0–65535 zero-based by default, or 1–65536 when ONE_BASED_ADDRESSES true), or Modicon notation (400001+, 300001+, 100001+). Multi-word types occupy consecutive PDU addresses from the resolved offset. See Address notation.
ADDRESS_TYPE
string
required
Which bank the tag lives in: HOLDING_REGISTER, INPUT_REGISTER, COIL, or DISCRETE_INPUT.
DATA_TYPE
string
default:"INT16"
Wire type: BOOL, INT16, UINT16, INT32, UINT32, INT64, UINT64, FLOAT, DOUBLE, or STRING. INT32/UINT32/FLOAT span 2 registers; INT64/UINT64/DOUBLE span 4.
SOURCE_TOPIC
string
MQTT topic whose publishes set this register or coil. Defaults to {mapping SOURCE_TOPIC}/{TagName}.
WRITABLE
boolean
default:"false"
Allow masters to write the address. Only HOLDING_REGISTER and COIL are master-writable.
DESTINATION_TOPIC
string
Topic published to when a master writes this (writable) address.
BYTE_ORDER
string
default:"BIGENDIAN"
Byte order within each register for multi-byte types: BIGENDIAN or LITTLEENDIAN.
WORD_ORDER
string
default:"BIGENDIAN"
Word order across registers for 32-/64-bit types (high word first by default): BIGENDIAN or LITTLEENDIAN.
DECIMAL_PLACES
integer
default:"2"
Rounding applied to FLOAT/DOUBLE values published to DESTINATION_TOPIC.
PERSISTENCE
boolean
Per-tag override of the route persistence default. Precedence: tag → mapping → route. See Data persistence & recovery.
DESCRIPTION
string
Free-text documentation for the tag.
EVERY
duration
default:"1 second"
On the mapping block: register → MQTT sync cadence for master writes.

All four address types

One server exposing every bank type:
DEFINE ROUTE MixedServer WITH TYPE MODBUS_SERVER
    ADD MAPPING Points
        WITH SOURCE_TOPIC "plant/mixed"
        ADD TAG Pressure
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
        ADD TAG MotorSpeed
            WITH ADDRESS "50"
            WITH ADDRESS_TYPE "INPUT_REGISTER"
            WITH DATA_TYPE "UINT16"
        ADD TAG PumpEnable
            WITH ADDRESS "0"
            WITH ADDRESS_TYPE "COIL"
            WITH DATA_TYPE "BOOL"
            WITH WRITABLE "true"
            WITH DESTINATION_TOPIC "plant/mixed/pump/cmd"
        ADD TAG DoorClosed
            WITH ADDRESS "0"
            WITH ADDRESS_TYPE "DISCRETE_INPUT"
            WITH DATA_TYPE "BOOL"
All four points are fed from plant/mixed/<TagName>; only PumpEnable accepts master writes (→ plant/mixed/pump/cmd).

Protocol Conformance

This server implements Modbus Application Protocol Conformance Class 0 and Class 1 over Modbus/TCP—the interoperability bar for standard masters:
ClassFunction codesSupported
Class 0FC 03 Read Holding Registers, FC 16 Write Multiple Registers
Class 1FC 01 Read Coils, FC 02 Read Discrete Inputs, FC 04 Read Input Registers, FC 05 Write Single Coil, FC 06 Write Single Register
Class 2FC 15 Write Multiple Coils
Writes are accepted only on WRITABLE HOLDING_REGISTER/COIL addresses. Undeclared addresses and writes to read-only banks return exception 02 (Illegal Data Address).
Modbus/TCP is plaintext—the protocol has no native authentication or encryption. Secure deployments by binding to a specific interface and restricting access at the network layer (firewall, VLAN, VPN). Modbus/TCP Security (TLS on port 802) is not yet implemented.

Data Persistence & Recovery

By default a server route holds register and coil values in memory only—a broker restart resets every address to zero. Set WITH PERSISTENCE true on MODBUS_SERVER_CONFIG to snapshot served values and restore them on startup.

Ownership-aware policy

What is safe to persist depends on who owns the value:
BankOwnerDefaultOn restart
HOLDING_REGISTER / COIL, WRITABLE trueMaster-written setpoint lives only in the slavePersistedRestored authoritatively—wins over retained MQTT
INPUT_REGISTER / DISCRETE_INPUT, and non-writable banksFed from live MQTTNot persistedRecovered from live MQTT (retained message / next publish)
Master-written setpoints survive a restart; live telemetry always reflects the current MQTT source.

Per-tag overrides

WITH PERSISTENCE true|false on an individual ADD TAG overrides the route default:
DEFINE ROUTE OverrideServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH PORT 502
        WITH PERSISTENCE true
    ADD MAPPING Registers
        WITH SOURCE_TOPIC "plant/line1"
        ADD TAG Setpoint
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH WRITABLE "true"
        ADD TAG LastGoodTemp
            WITH ADDRESS "200"
            WITH ADDRESS_TYPE "INPUT_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH PERSISTENCE "true"
        ADD TAG ScratchSetpoint
            WITH ADDRESS "300"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "INT16"
            WITH WRITABLE "true"
            WITH PERSISTENCE "false"
  • Setpoint — restored authoritatively to the last master-written value.
  • LastGoodTemp — served from snapshot until plant/line1/LastGoodTemp next publishes.
  • ScratchSetpoint — always comes back at zero.

Caching slow or infrequently-updated sources

Broker-fed tags are not persisted by default. When a source updates infrequently—an hourly energy total, a shift-latched value—force-persist with WITH PERSISTENCE "true" so the last received value is re-served on cold start until the next live publish overrides it.
DEFINE ROUTE MeterServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH PORT 502
        WITH PERSISTENCE true
    ADD MAPPING Meters
        WITH SOURCE_TOPIC "plant/energy"
        ADD TAG TotalEnergy
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "INPUT_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH PERSISTENCE "true"
If every tag in a mapping is a slow source, set WITH PERSISTENCE "true" once on the ADD MAPPING block instead of per tag.

Storage & durability

  • Values are written through a non-blocking background worker on every change (rapid updates coalesce to the latest value).
  • The snapshot lives in an encrypted-at-rest database under the broker data directory.
  • A snapshot is keyed to the route’s tag definitions by a fingerprint. If addresses or types change between runs, the stale snapshot is discarded rather than replayed into reshaped addresses.

Verify Your Server

1

Deploy the route

Add the route definition to your broker and confirm route status shows Connected (the endpoint is listening).
2

Connect with a Modbus master

Use any standards-compliant Modbus TCP master. Connect to <HOST>:<PORT> and address the route’s UNIT_ID (default 1).
3

Confirm MQTT → register

Publish a test value to a bound SOURCE_TOPIC. Read the corresponding address from the master and confirm the decoded value matches.
4

Confirm register → MQTT

For a writable tag with a DESTINATION_TOPIC, write a value from the master. Subscribe to the destination topic with MQTT Explorer or any MQTT client and confirm the publish arrives within one WITH EVERY interval.

Limitations

Each MODBUS_SERVER route binds exclusively to its PORT. A second route on the same port fails to start. Give each route its own port.

Troubleshooting

Publish -checkRouteConnection <RouteName> to $SYS/Coreflux/Command. For a server route this reports whether the hosted endpoint is listening, along with the endpoint, port, unit ID, and current connection count.
  • Port in use — Another route or process owns PORT. Choose a free port or stop the conflicting service.
  • Bind error — Check broker logs. Ensure HOST is a valid interface on the machine.
Confirm HOST/PORT, network path, and firewall rules. The master must target the route’s UNIT_ID (default 1).
Registers default to zero until first set. Verify SOURCE_TOPIC has published; retained MQTT values are seeded at startup.
  • Confirm the tag is WRITABLE on a HOLDING_REGISTER or COIL with a DESTINATION_TOPIC.
  • The value surfaces within one WITH EVERY interval—verify the cadence.
Read-only banks (INPUT_REGISTER, DISCRETE_INPUT) and non-writable tags reject master writes by design. Undeclared addresses return Illegal Data Address.
Align BYTE_ORDER and WORD_ORDER with the master’s expectation. Default is big-endian, high word first.
Confirm tag ADDRESS values match how the master addresses the server. Use WITH ONE_BASED_ADDRESSES true when your tag labels follow device-manual numbering (register 1 = address "1"). Modicon notation (400100) works regardless of the flag.
MAX_CONNECTIONS is a best-effort soft cap—the underlying library exposes no per-connection control, so exceeding the cap is logged rather than enforced at accept time. Restrict concurrency at the network layer if a hard cap is required.

Best Practices

Prefer WITH HOST "192.168.10.5" over 0.0.0.0 when only one network should reach the server.
Use WITH PORT GET ENV "MODBUS_PORT" instead of hard-coding environment-specific ports.
Only declared addresses are served. One ADD TAG per register or coil a master needs.
Leave WRITABLE unset for read-only points. Non-writable holding registers and read-only banks reject writes with a Modbus exception.
Set WORD_ORDER "LITTLEENDIAN" when the master expects low word first. Default is big-endian, high word first.
Use a shorter interval for responsive control loops; a longer one to reduce churn for slowly-changing values.
Document which route owns which port to avoid startup conflicts when adding routes.
Modbus TCP has no authentication. Place the server behind a firewall or VLAN and rely on HUB topic permissions for MQTT-side security.
Watch $SYS/Coreflux/Routes/{RouteName}/status. Connected means the server is listening—investigate Error immediately (usually a port conflict).

Next Steps

Modbus TCP Client

Connect to remote Modbus devices and poll register data into MQTT.

Modbus Serial Client

Connect to remote Modbus RTU devices over serial.
Last modified on July 3, 2026