How to connect a PLC to the cloud over Modbus TCP

Zayin Krige, 2026-08-26. 6 min read.

To connect a PLC to the cloud, put a small gateway on the same local network as the PLC. The gateway acts as a Modbus TCP client that polls the PLC on port 502, and it forwards what it reads to your backend over HTTPS. The PLC never talks to the internet and the cloud never talks Modbus. The gateway translates between the two and buffers every event on local disk, so a dropped connection or a power cut loses nothing. A Raspberry Pi running a single Go binary is enough hardware for the job.

This is the design I built for OmniVerify, an industrial dispense-control platform. The cloud backend authorises each dispense and a PLC runs the pump.

The architecture: PLC, gateway, backend

The Modbus TCP basics you need

The Modbus.org TCP/IP implementation guide reserves TCP port 502 for Modbus. The PLC is the server (older documentation says slave). It exposes coils, which are single bits, and holding registers, which are 16-bit words. There is no push, so the gateway learns everything by polling. It needs five function codes from the Modbus application protocol specification.

CodeFunctionWhat the gateway uses it for
01Read CoilsThe status bank: session active, each outcome, alarms, all in one request
03Read Holding RegistersLitres, counters, timestamps and PLC settings
05Write Single CoilStart a session, reset an alarm, request a clock sync
06Write Single RegisterThe authorised setpoint and the manual-override password
16Write Multiple RegistersValues that span two registers, such as a 32-bit session ID, and the clock

Word order is not in the spec

The specification defines registers as 16 bits. A float32 litre value or a 32-bit counter therefore spans two registers, and the order of those two words is whatever the PLC programmer chose. Ours is low word first and checked against the HMI display. I pinned it with a test that decodes a snapshot captured from the live PLC, because a flipped word order produces a plausible wrong number, not an error.

An exception response is not a dropped connection

A Modbus exception means the socket is healthy and the PLC rejected the request. Only a transport failure should trigger a reconnect, with backoff from 500 ms up to 30 seconds. Some PLCs refuse new connections after a handful of reconnects, so a client that re-dials on every error can lock itself out.

Polling: read the whole coil bank at once

The gateway polls once a second and reads every status coil it needs in a single Read Coils request over one contiguous range. Reading coils one at a time tears across PLC scan cycles: you can see the finished bit from one scan and the result bit from the next. A session end is the falling edge of the session-active coil. Fetching new instructions from the backend runs on its own slower loop, every three seconds: a one-second fetch is 86,400 requests a day, mostly for empty responses.

Measured live calls averaged about a millisecond, so a 500 ms per-call deadline leaves wide headroom without letting a hung read blind the gateway for long.

Writing to the PLC: the setpoint handshake

A dispense cannot be undone, so the write path is ordered to rule out a double dispense.

  1. Fetch the pending instruction from the backend and validate it. The setpoint must be a whole number of litres, because the register holding it is an integer.
  2. Refuse if the PLC already reports an active session.
  3. Write the setpoint, the manual-override password and the session ID.
  4. Record the instruction as applied in the local database before touching the start coil. Otherwise a power cut just after the pulse lets the next boot apply it again.
  5. Raise the start coil and hold it high until the PLC reports the session active, then drop it. A fixed-length pulse is not guaranteed to line up with the PLC's scan cycle.
  6. Acknowledge the instruction to the backend.

Read the PLC's verdict, do not recompute it

The PLC knows whether a dispense completed, stopped early, or never flowed, and it raises an alarm when flow happens without an authorised instruction. It publishes each outcome on its own coil. The gateway takes the outcome from those coils and does not reproduce the PLC's decision logic. Two copies of that logic will eventually disagree.

Timing matters here. The PLC decides whether unauthorised flow was high or low only after a configurable timer window. The coil that resets the alarm also clears that verdict. Resetting early erased the verdict before the PLC wrote it, so the gateway holds the alarm record across poll ticks until the window has elapsed, then records the verdict, then resets.

Offline first: a SQLite WAL queue and idempotent sync

Keep the PLC clock honest

PLC timestamps end up in an audit trail. A Raspberry Pi has no real-time clock, so after a cold boot its time can be wrong by decades until NTP syncs. The gateway pushes the time to the PLC once an hour, only after the Pi's clock is confirmed synchronised and never while a transaction is open. It writes the time registers first, then sets a request coil. The PLC applies the time and clears the coil itself.

Running a Raspberry Pi Modbus gateway unattended

Where the cloud side fits

Once events arrive over HTTPS, the backend is ordinary server work. For that half, see designing an event-driven serverless platform on AWS. More of my projects are on the homepage.

I take on fractional CTO and senior architecture contracts through Apex Technology, working remotely. Email info@zayinkrige.com or read the full CV.