All posts
rustengineering

Why we wrote a monitoring agent in Rust instead of Go

By SecuryBlack

Go is the default choice for infrastructure tooling in 2025. Prometheus, Grafana Agent, Telegraf, Docker, Kubernetes — the list is long. When we started OxiPulse, we seriously considered Go. We chose Rust. Here's why.

The core constraint: resource overhead

A monitoring agent runs on every server you own. On a $4/month VPS with 512 MB of RAM and one shared vCPU, a 200 MB RSS agent is not acceptable. The agent itself becomes a thing to monitor.

Go's runtime carries a garbage collector. For most workloads this is invisible. For a long-running daemon that allocates metric structs every 10 seconds, GC pauses are rare but non-zero. More importantly, Go's minimum RSS on a real workload tends to be 20–50 MB just for the runtime, heap, and goroutine stacks.

Rust has no runtime and no garbage collector. OxiPulse's RSS in steady state is under 8 MB. On a constrained edge node or a Raspberry Pi, that difference matters.

Single static binary, for real

Go produces statically-linked binaries by default — unless you use CGO, which most network and system libraries do. Cross-compiling a CGO binary for Linux ARM64 from macOS requires a full cross-compilation toolchain and is notoriously fragile.

Rust's static linking story is simpler. OxiPulse targets x86_64-unknown-linux-musl and aarch64-unknown-linux-musl, which produce fully static binaries with zero shared library dependencies. The same binary runs on Alpine, Debian, Ubuntu, RHEL — any Linux distribution.

The OpenTelemetry SDK

OxiPulse speaks OTLP natively. The official Rust OpenTelemetry SDK is mature, async-native (built on Tokio), and integrates cleanly with Tonic for gRPC. There is no need for a separate exporter process or a sidecar.

The Go OpenTelemetry SDK is also production-quality, so this was not a deciding factor on its own.

What Go would have been better for

We are not anti-Go. If OxiPulse needed:

  • A large number of plugins with dynamic dispatch (Telegraf's model)
  • Fast iteration on protocol integrations
  • A big contributor community (Go's tooling ecosystem is excellent)

…Go would have been the right call. For a focused, single-purpose binary where memory footprint and startup time matter more than plugin breadth, Rust was the better fit.

The tradeoff we accepted

Rust has a steeper learning curve and a slower compile cycle. Our CI build takes longer than it would in Go. The borrow checker caught real bugs during development — which was the point — but it also slowed initial implementation.

For a tool that runs unattended on thousands of servers, correctness and efficiency outweigh developer convenience. That tradeoff made sense for us.