Tunas Akara
Back to Blog

Why We Write IoT Gateways in Go

One self-contained file, built for ARM from a laptop, about 10 MB with database and web UI inside. Why Go fits site gateways, and what it doesn't fix.

by RayhanUpdated 8 min read

For Technical Teams

iotgoedgegatewaydeployment
Why We Write IoT Gateways in Go
On this page

A gateway is the small on-site box between your sensors and your server. It often runs for years on inexpensive hardware that rarely gets an on-site visit. Go fits that job because it can compile into one self-contained file, with no separate runtime to install. You can build for ARM from a laptop; our gateway builds are about 10 MB. The catch: security updates require rebuilding and deploying the program.

That last sentence matters as much as the rest. Go removes a whole class of field problems and adds one chore. Both are covered below.

What does a gateway actually do all day?

Four things, on repeat. It reads devices over whatever they speak (RS485, Modbus, MQTT, a vendor's TCP protocol). It holds data locally when the internet drops. It pushes readings to the server when the link comes back. And it receives commands, such as switching a breaker off, and reports whether they actually happened.

None of that is computationally heavy. What makes it hard is the setting. The box sits in a panel room or a ceiling, on mains power that sags and spikes, behind a 4G modem with one bar. Nobody logs into it for months. When something goes wrong, the fix has to travel over the same bad link.

The requirements are practical: recover after power cuts, use little memory, support remote updates and rollback, and avoid dependencies that might be missing on the device.

What is a "single binary" and why does it matter on site?

Think of a packed lunch versus a meal that needs the kitchen. A Python or Node.js program is the meal: the device needs the right interpreter, the right packages, and the right system libraries before your code can run. Every one of those is something that can be missing, a different version, or broken by an OS update.

A Go program is the packed lunch. The compiler bundles your code, the libraries, and the runtime into one file. Copy it to the box, run it. Nothing else to install.

We measured this on a real tool: the controller we built for a family of smart circuit breakers. One file holds the MQTT client, a SQLite database engine, an HTTP server, and the technician's web dashboard. Built with CGO_ENABLED=0 and stripped of debug symbols, it comes out at 10.5 MB for 64-bit ARM and 10.7 MB for 32-bit ARMv7, and the file command reports it as "statically linked". No shared library on the device is involved.

Runtime drift is not a theoretical problem. Node.js 20 reached end of life on 30 April 2026; devices shipped with it either get a runtime upgrade in the field, with every package re-tested against it, or keep running unpatched. A Go binary carries its runtime inside, so it cannot drift independently from the application. Security fixes still require rebuilding and redeploying it.

How do you build for an ARM box from a laptop?

Cross-compiling means building a program for a different kind of chip than the one you are sitting at. In Go, set the target operating system and architecture, along with the cgo and ARM-version options:

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags="-s -w" -o dist/gw-arm64 .
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -trimpath -ldflags="-s -w" -o dist/gw-armv7 .

Go 1.26 lists 47 operating system and chip combinations in go tool dist list, 13 of them for Linux alone, including ARM, MIPS (common in routers), and RISC-V. Each build of the breaker controller took 7 to 14 seconds on an ordinary laptop. No ARM board, no emulator, no Docker image.

When does building for another chip get hard?

At cgo, the bridge that lets Go call C code. The cgo documentation is explicit: cgo is disabled by default when cross-compiling. If one dependency needs C, you now need a C compiler for every target chip, and the easy build is gone.

We hit exactly that split on a remote-execution agent we ship. The desktop version has a GUI toolkit written against C, so building it for Windows from Linux needs a MinGW toolchain or a Docker-based builder. The headless (no-GUI) build for Linux ARM sets CGO_ENABLED=0, skips the GUI, and cross-compiles in seconds.

The rule we follow for anything that runs on a gateway: pure-Go libraries only. modernc.org/sqlite instead of the C-based SQLite driver, the Eclipse Paho client for MQTT, the standard library for HTTP. Then set CGO_ENABLED=0 in CI, your automated build pipeline, so a dependency that sneaks C in breaks the build loudly instead of producing a binary that fails on the device.

What does a Go gateway look like inside?

Loading diagram…

Each device connection gets its own goroutine: a lightweight worker that costs a few kilobytes, so a gateway can run hundreds of them without thinking about threads. When one meter hangs, its worker waits; the others keep reading.

Two habits matter more than the language itself.

Treat every command as possibly lost. The breaker gateway we integrated publishes commands at MQTT QoS 0, which means fire and forget: if the gateway is offline, the command simply disappears. The controller waits 15 seconds for a reply, then marks the command failed. It also watches for the breaker's own status change, because the gateway answering "OK" and the relay actually moving are two different events. Go's context timeouts make this pattern short to write, but the decision to write it is yours.

Buffer before you send. Readings go into the local database first and leave from there. An uplink outage then delays delivery while the local buffer has space and remains intact. The single-point-of-failure checklist for gateways covers how big that buffer should be.

Go's embed package puts the HTML of the technician's page inside the binary as well. The page ships and updates with the program, so the deployed page and program share a version.

How do updates work when the box is 500 km away?

Single binary makes the update a file swap:

  1. Download the new binary to a temporary file on the same disk.
  2. Check its SHA-256 checksum, a fingerprint of the file, against the value your server published through a trusted channel. Verify the package or manifest signature too; a checksum does not authenticate origin if an attacker can replace both file and checksum.
  3. Copy the running binary aside as gw.prev, then rename the new file over the old one. A rename on the same filesystem is atomic: the box sees either the old file or the new one, never half of each.
  4. Restart the service (systemd, the Linux service manager, does this in one line).
  5. If the new binary fails its health check (an automatic "am I working?" test) within a few minutes, rename gw.prev back and restart again.

Stamp the version into the binary at build time with -ldflags "-X main.version=1.4.2" so gw --version answers what is running in the field. Write the downloaded file once and avoid rewriting it; cheap storage on these boxes wears out from writes.

What does Go not solve?

Security patches become your job. On a normal server, a vulnerability in OpenSSL or Python gets fixed by the OS package manager. A static Go binary contains its own copy of the runtime and libraries, so it stays vulnerable until you rebuild and redeploy it. Go's release policy supports each major version only until two newer ones exist. Go 1.27 shipped on 19 August 2026, and that same day Go 1.25 received its last patch, 1.25.14. A gateway built with Go 1.25 in 2025 and never rebuilt has used an unsupported runtime since then.

The fix is a calendar entry, not a rewrite: rebuild on every Go minor release, run govulncheck (Go's official vulnerability scanner) in CI, and roll out through the update path above. If updating is painful, that is the thing to fix first.

Microcontrollers are a different world. A 10 MB binary assumes a Linux-class box: an industrial PC, a router, a Raspberry Pi-type board. The chip inside the sensor itself still runs C or the vendor's SDK.

The team still has to read it. Go is easy to learn, but a gateway written in a language nobody on the team knows becomes the one system everyone avoids. The language choice belongs to the same decision as the rest of your stack.

When is Python or Node still the right call?

When the gateway mostly runs a machine-learning model, Python's libraries win and the runtime is a price worth paying. When the vendor only ships a Python SDK, wrapping it is cheaper than reimplementing it. And for a two-week pilot on one site, use whatever gets data flowing fastest. Move to Go when the box count, or the distance to the nearest box, starts to hurt.

Checklist for a gateway build

  • CGO_ENABLED=0 in CI, so a C dependency fails the build, not the device.
  • One build per target chip (arm, arm64, amd64) from the same commit.
  • Version stamped into the binary and reported to the server on connect.
  • Updates with checksum and signature verification, rename, and restart, with the previous binary kept.
  • Every command has a timeout and a recorded result.
  • Readings buffered locally before upload.
  • A rebuild on every Go minor release, with govulncheck in the pipeline.

What should you ask before buying a gateway?

Request supported chips, power and internet-loss test results, and rollback after a failed update. Agree who rebuilds for security patches. A small binary helps delivery; recovery evidence and support duration determine operating cost.

Related Posts

Need help applying this approach?

IoT Device Monitoring System Development

Bring meter, sensor, and other device data into one dashboard. Monitor recent readings, historical data, and disruptions around your operational needs.

Explore related services

Describe the operational problem you are facing so we can discuss a suitable next step.

Share this article
Free Initial Discussion