Rust on ESP32 and Single-Board Computers: Our Embedded Stack

My experience with low-level embedded hardware began years ago, writing C and C++ for microcontrollers where memory safety was a constant battle against buffer overflows and silent memory corruption. In The Lean Stack, I described returning to fundamentals on the web. This is the companion piece about the devices I build: sensor nodes, thermal controllers, kiosks, and edge boxes. Everything I deploy today on edge hardware is written in Rust.
Why embedded development needed a reset
Embedded development has long suffered from toolchain bloat: vendor IDEs from the 2000s, C codebases where one stray pointer silently corrupts a sensor reading for weeks, or the opposite extreme of running MicroPython and Node on a Raspberry Pi where firmware is an interpreter, a package manager, and a hope that nothing breaks on update.
Rust eliminates both failure modes:
- No runtime or garbage collector. The binary flashed to the device is the entire program. Timing is predictable, memory usage is known at compile time, and there is no runtime interpreter to fail.
- Memory safety at compile time. Entire classes of bugs that plague embedded engineering (buffer overruns, use-after-free errors, and data races between interrupts and the main loop) fail to compile.
- Shared types from microcontroller to server. The same types that define a sensor reading on the chip can be shared with the ingestion service. This eliminates reimplementing the same packet format across C, Python, and TypeScript.
Rust on the ESP32
The ESP32 family is our default microcontroller: inexpensive, widely available, with Wi-Fi and Bluetooth on chip. The Rust ecosystem for ESP32 has matured into production across two primary tracks:
- esp-hal (bare metal, no_std): pure Rust directly against the hardware. Minimal footprint, full control, ideal for battery-powered sensor nodes that read, transmit, and sleep.
- esp-idf with std: Rust on top of Espressif's official framework. This retains the mature Wi-Fi, MQTT, and TLS stacks while allowing application logic to be written in safe Rust with threads, standard string types, and the wider crate ecosystem.
We use the second track for most commercial deployments: reliable networking comes from the vendor framework, and the core application logic (calibration, batching, and failure handling) runs in safe Rust where the compiler validates it.
The toolchain is straightforward: cargo build, flash over USB, and run. It follows the standard Cargo workflow used on backend servers, without vendor IDE dependencies.
Rust on single-board computers
One level above microcontrollers sit single-board computers (Raspberry Pi and industrial equivalents) running gateways, local dashboards, and on-site telemetry ingestion.
A common default is running Python scripts under systemd. This works initially but introduces risks: dependency drift, Linux distribution updates that alter the interpreter, or gradual memory leaks in long-running processes.
Rust cross-compiles to a single static binary. Deploying means copying one file to the board, matching the simplicity that Bun provides on the web tier. For an SBC deployed at a customer facility, a static binary without runtime dependencies eliminates common maintenance overhead.
These boards also run our local AI workloads: compact models running anomaly detection or vision inference on-site, with the Rust service feeding inputs and sending concise structured conclusions upstream to self-hosted infrastructure rather than streaming raw sensor feeds.
The AI agent workflow with Rust
AI coding agents work remarkably well with Rust.
Rust is often considered a difficult language, but AI agents benefit from clear, deterministic compiler feedback. When an agent drafts code, the borrow checker pinpoints exact safety violations. The agent adjusts the code against compiler errors, ensuring that once it compiles, whole categories of memory bugs are already resolved. In contrast, C code compiles easily but defers memory corruption bugs to runtime.
For a solo engineer shipping firmware, the division of labor is effective: the compiler enforces invariants, the agent drafts the boilerplate, and the engineer directs the architecture.
Practical trade-offs
Rust comes with real trade-offs. Compile times are longer than C. The ecosystem for niche peripheral drivers is smaller, occasionally requiring an unsafe wrapper around vendor C libraries. The borrow checker also requires an initial adjustment period.
However, these costs are paid upfront during development on a local machine. The alternative in C often defers costs to debugging deployed hardware in the field.
Predictable execution across tiers
The lean stack principle applies across all tiers: maintain the fewest layers necessary to govern the system. On the web that means HTML and Bun. On physical devices it means Rust: single static binaries, no runtime dependencies, and compile-time verification of memory safety.
From a 240MHz microcontroller to a self-hosted server rack, consistent fundamentals keep systems reliable.