Azure IoT Modbus Gateway Testing

Getting Modbus data into Azure has two different answers depending on which Azure edge platform you're targeting, and the gap between them matters more in 2026 than it did a couple of years ago. Classic Azure IoT Hub has a purpose-built module for it. Azure IoT Operations, Microsoft's newer Arc-enabled edge platform, doesn't — and that changes how you should architect the bridge. This guide covers both paths: configuring and testing the legacy iot-edge-modbus module against IoT Hub, and what your options actually are if you're building on IoT Operations instead.

Two Azure Edge Platforms, Two Different Answers

Before writing a line of configuration, figure out which Azure edge stack you're actually targeting, because the two current options diverge sharply on Modbus support:

PlatformModbus supportStatus
Azure IoT Hub + IoT Edgeiot-edge-modbus module (prebuilt, deploy via Marketplace or custom deployment manifest)Functional, but its deployment guide now lives under a Microsoft Learn "previous-versions" URL tied to IoT Edge 1.5 — no longer the actively promoted path
Azure IoT Operations (Arc-enabled)No first-party Modbus connectorNative support exists for OPC UA (dedicated connector) and MQTT (built-in broker); Modbus requires a third-party component or a custom bridge

If you're maintaining an existing IoT Hub deployment or starting a straightforward gateway project without a hard requirement to be on IoT Operations, the iot-edge-modbus module remains the fastest path and is what the rest of this guide focuses on first. If you're building new on IoT Operations, skip ahead to the IoT Operations section — the module described below won't plug into it.

The iot-edge-modbus Module

iot-edge-modbus is an IoT Edge module, source published under the Azure GitHub organization, that polls Modbus TCP or RTU slaves on a schedule and publishes the collected values as IoT Hub messages through the IoT Edge runtime's routing. You deploy it like any IoT Edge module — either by searching the Azure Marketplace for the "Modbus TCP Module by Microsoft" from your IoT Hub's device deployment blade (which auto-wires the IoT Hub routes for you), or by adding it directly to a deployment manifest if you're managing deployments as JSON/CI artifacts.

Once deployed, all the actual behavior — which device to poll, which registers, how often — is controlled through the module's desired properties (its module twin), which you can push through the Azure portal, the Azure CLI, or IoT Hub's device twin APIs.

Configuring SlaveConfigs and Operations

The module's configuration is a JSON object built around SlaveConfigs, keyed by an arbitrary slave name:

{
  "PublishInterval": "5000",
  "SlaveConfigs": {
    "Slave01": {
      "SlaveConnection": "192.168.1.50",
      "RetryCount": "10",
      "RetryInterval": "50",
      "Operations": {
        "Op01": {
          "PollingInterval": "1000",
          "UnitId": "1",
          "StartAddress": "40001",
          "Count": "10",
          "DisplayName": "Line1_HoldingRegs",
          "CorrelationId": "hr-block-1"
        },
        "Op02": {
          "PollingInterval": "2000",
          "UnitId": "1",
          "StartAddress": "30001",
          "Count": "4",
          "DisplayName": "Line1_InputRegs",
          "CorrelationId": "ir-block-1"
        }
      }
    }
  }
}

Key fields:

FieldScopeMeaning
SlaveConnectionPer slaveIPv4 address (TCP) or serial port name (RTU) of the Modbus device
RetryCountPer slaveMax retry attempts on a failed read (default 10)
RetryIntervalPer slaveMilliseconds between retries (default 50)
PollingIntervalPer operationHow often that specific register block is read, in milliseconds
UnitIdPer operationModbus slave/unit address — matters when polling through a gateway bridging multiple RTU devices onto one TCP connection
StartAddress / CountPer operationThe register range to read for that operation
DisplayNamePer operationThe tag name attached to the value in the published IoT Hub message

As with any Modbus integration, watch the addressing convention: StartAddress values here follow the Modicon-style documented register numbering in some sample configs (e.g. 40001 for the first holding register) while the underlying wire read is zero-based — always confirm which convention a given deployment expects against a register you can independently verify, rather than assuming.

PollingInterval vs. PublishInterval

These two settings are easy to conflate but control different things. PollingInterval is set per operation and controls how often the module actually reads that register block off the wire. PublishInterval is set once, module-wide, and controls how often the module batches whatever it has collected since the last publish and sends it to IoT Hub as a message.

They're deliberately decoupled: you might poll a fast-changing analog value every 500ms internally for local accuracy, but only publish to the cloud every 5 or 10 seconds to control IoT Hub message volume and ingestion cost. Get this backwards — publishing faster than you poll, or polling far faster than you ever publish — and you either waste cloud messages on unchanged data or throw away resolution you paid the polling cost for. Size both deliberately against your actual data-freshness requirement, not just the module's defaults.

Azure IoT Operations: No Native Modbus Connector

If you're building on Azure IoT Operations — Microsoft's newer, Arc-enabled edge platform built around Kubernetes and a local MQTT broker rather than classic IoT Hub device connections — the picture changes. As of 2026, Azure IoT Operations ships a first-party connector for OPC UA and native MQTT support through its built-in broker, but it has no equivalent first-party Modbus connector.

That leaves two realistic options for getting Modbus data into an IoT Operations deployment:

  • A third-party MQTT-capable edge component that speaks Modbus — HiveMQ Edge is a commonly cited example, positioned specifically as a protocol-translation layer that can poll Modbus and publish onward to an MQTT broker, including the one IoT Operations uses.
  • A custom bridge you build and run as its own edge workload — a service (Python with pymodbus, or any language with a Modbus client library and an MQTT client) that polls Modbus registers directly and publishes to the local MQTT broker in whatever topic/payload structure your IoT Operations pipeline expects downstream.

Neither option is a drop-in replacement for the old iot-edge-modbus module's "deploy and configure JSON" simplicity — both require more integration work up front. If your project has flexibility on which Azure edge platform to target and Modbus is a primary protocol, that's a real factor worth weighing before committing to IoT Operations over classic IoT Hub + IoT Edge.

Building a Custom Bridge Instead

Whether you're targeting IoT Operations' MQTT broker or you just need logic the prebuilt module doesn't offer (unit conversion, conditional publishing, combining Modbus data with a second data source before sending), a custom bridge is usually a Modbus client library plus an Azure connectivity SDK, wired together in a poll loop:

# Minimal structure — poll Modbus, publish to Azure
from pymodbus.client import ModbusTcpClient
from azure.iot.device import IoTHubDeviceClient, Message
import json, time

modbus = ModbusTcpClient('192.168.1.50', port=502)
azure = IoTHubDeviceClient.create_from_connection_string(CONN_STR)
azure.connect()

while True:
    result = modbus.read_holding_registers(address=0, count=10, slave=1)
    if not result.isError():
        payload = {"line1_holding_regs": result.registers}
        azure.send_message(Message(json.dumps(payload)))
    time.sleep(5)

This is the same shape whether the target is classic IoT Hub (via the Azure IoT device SDK, shown above) or an IoT Operations MQTT broker (swap the Azure IoT Hub client for a standard MQTT client pointed at the broker's endpoint and topic structure). The Modbus half of the code — connecting, reading registers, handling exceptions — doesn't change based on which Azure platform receives the data.

Common Errors and Fixes

SymptomLikely CauseFix
Module deploys but no data arrives at IoT HubSlaveConnection unreachable from the IoT Edge device, or PublishInterval set very longVerify network reachability from the edge device itself (not your workstation), and check whether data is simply queued for the next publish cycle
Values look off by one registerStartAddress using documented (1-based) numbering where the module expects zero-based, or vice versaTest against a simulator register you set to a known, distinctive value and adjust the offset until it matches
Reads fail intermittently on an RTU deviceRetryInterval too short for a slow serial device's response timeIncrease RetryInterval and/or RetryCount for RTU slaves specifically
Module twin changes don't take effectDesired properties updated but module not restartedRestart the module (or redeploy) after any desired-properties change — this module doesn't always hot-reload configuration
Wrong device responds when polling through a gatewayUnitId mismatch on a shared TCP connection bridging multiple RTU unit addressesConfirm the UnitId for each operation matches the specific device behind the gateway, not just the gateway's own IP

Testing Against a Simulator Before Real Hardware

Point SlaveConnection at a Modbus TCP slave simulator's IP and port instead of a real device, whether you're testing the prebuilt module or a custom bridge:

IoT Edge Device (Modbus module or custom bridge)  →  TCP 502  →  Modbus Slave Simulator
[deployed to a test device or VM]                                      [ModbusSimulator on a bench machine]

Configure ModbusSimulator to run as a Modbus TCP slave on the address and port your configuration points to, and preload registers with distinctive, non-zero test values. Then work through the checks that are hard to safely reproduce against live equipment:

  • Addressing verification: confirm each Operation's StartAddress reads the register you intended, catching the 1-based/zero-based offset before it reaches production
  • End-to-end confirmation: watch messages arrive at IoT Hub with Azure IoT Explorer or az iot hub monitor-events, and confirm DisplayName tags and values match the simulator's preloaded data
  • Timing validation: change a simulator register value and time how long it takes to appear at IoT Hub, confirming PollingInterval and PublishInterval combine to the latency your project actually needs
  • Failure handling: stop the simulator mid-poll and confirm the module's RetryCount/RetryInterval behave as configured rather than crashing the module, then restart the simulator and confirm recovery
  • Multi-slave/UnitId checks: if polling through a gateway with multiple UnitIds, configure the simulator to respond on more than one unit address and confirm each Operation reads the correct one

Doing this on a bench before deploying to a real IoT Edge gateway at a live site catches configuration mistakes — wrong addresses, misconfigured retries, a PublishInterval that's silently too long — while they cost a five-minute redeploy, not a site visit. It's also the only practical way to test failure handling (a device going offline mid-poll) on demand, since forcing that condition against production equipment usually isn't an option.

Download ModbusSimulator to Test Your Azure IoT Gateway →

Frequently Asked Questions

How do I connect a Modbus device to Azure IoT Hub?

Deploy the iot-edge-modbus module to an IoT Edge gateway device, configure SlaveConfigs with the device's connection details and which registers to poll, and the module publishes results as IoT Hub messages via the IoT Edge runtime.

Is the iot-edge-modbus module still current in 2026?

It still works, but its docs now live under a Microsoft Learn previous-versions URL tied to IoT Edge 1.5, and Azure's newer platform (Azure IoT Operations) has no native Modbus connector — treat it as a legacy path for classic IoT Hub deployments, not Microsoft's current investment area.

How is the Modbus module configured?

Via desired-properties JSON structured around SlaveConfigs: SlaveConnection (IP or serial port), RetryCount/RetryInterval, and an Operations array specifying PollingInterval, UnitId, StartAddress, Count, and a DisplayName tag per register block. PublishInterval is set module-wide.

What's the difference between PollingInterval and PublishInterval?

PollingInterval (per operation) controls how often a register block is read off the wire. PublishInterval (module-wide) controls how often collected values are batched and sent to IoT Hub — the two are independent and should be sized separately.

Does the module support both Modbus TCP and RTU?

Yes — SlaveConnection accepts either an IPv4 address for Modbus TCP or a serial port name for Modbus RTU over an attached serial adapter. The Operations configuration format is identical between the two modes; only the connection string and physical link differ.

Can Azure IoT Operations talk to Modbus devices at all?

Not natively as of 2026 — it ships an OPC UA connector and MQTT broker but no first-party Modbus connector. Getting Modbus data in requires a third-party component (e.g. HiveMQ Edge) or a custom bridge publishing to the local MQTT broker.

How do I test a Modbus-to-Azure gateway without a real PLC?

Point SlaveConnection at a Modbus TCP slave simulator, deploy the module or bridge to a test IoT Edge device, and confirm messages arrive at IoT Hub with the expected values using Azure IoT Explorer or the CLI.

What commonly goes wrong when deploying a Modbus IoT Edge gateway?

Addressing offset mistakes, a RetryInterval too short for slow RTU devices, UnitId mismatches on shared gateway connections, and module twin changes not taking effect until the module restarts.

Should I use the Modbus module or write my own bridge?

The prebuilt module is faster for straightforward IoT Hub polling scenarios. A custom bridge makes sense for extra logic, or when targeting IoT Operations' MQTT broker instead of classic IoT Hub — prototype either against a simulator first.

Test Your Azure IoT Modbus Gateway Without Real Hardware

ModbusSimulator acts as a fully configurable Modbus TCP or RTU slave for validating the iot-edge-modbus module's configuration, addressing, and retry behavior — or a custom Azure bridge — before it ever touches a PLC. Free tier available.

Download Free tier → Learn More