The Argument For Rust On Arduino — Am I Crazy?
Arduino was never a language — it was a promise. Here's the case for keeping that promise and putting memory-safe embedded Rust underneath it, with receipts.

Let's get the obvious reaction out of the way. Yes, a bit. But stick around, because I have receipts and one genuinely embarrassing admission at the end.
Here's the thing nobody says out loud: Arduino isn't a language. It's a promise. setup(), loop(), digitalWrite(13, HIGH) — no build system, no error types, no explanation demanded of you. That promise is why half the working hardware on Earth is sitting on a desk next to a mug. It is one of the best pieces of developer-experience design of the last twenty years, and almost nobody credits it, because it was aimed at people who don't read Hacker News.
None of that promise is about C++. C++ is just what was lying around in 2005. So the argument for Rust on Arduino isn't an argument against the promise — it's an argument about what sits underneath it. At MATA we build in Rust for exactly the reason that applies here: the device in your house should not be the weakest thing on your network.
Arduino Isn't A Language — It's A Promise
Separate those two ideas and the whole debate about embedded Rust gets easier. The promise is the two-function mental model, the one-click flash, the example that compiles first time. The substrate is the compiler, the memory model, and the toolchain underneath it. People defend the first and assume they're defending the second.
Keep setup() and loop(). Change what's beneath them. Memory-safe embedded Rust gives you the same shape of sketch with a compiler that refuses to let a parser walk off the end of a buffer — which matters enormously the moment a device stops being a blinking LED and starts being something with a camera in a stranger's hallway. That's Era 4, Home Companion territory: smart-home hardware that answers to you instead of a vendor cloud.
Where Embedded Bugs Actually Live
Your blink sketch does not have a use-after-free. Nothing on a microcontroller does — right up until bytes arrive from somewhere you don't control. An RTP header. A JPEG marker. A FLAC header. A radar module's serial protocol. That is where devices die, and it is the only part of the argument for Rust on Arduino that really matters.
So I made a rule — parsers return errors, never panic — and turned the rule into a test that throws random garbage and mutated packets at every parser across seven packages. The IETF's RTP specification is public, which means an attacker knows the header layout exactly as well as you do. Four packages came back clean. Three did not.
A Fuzzer, Seven Packages, Three Real Bugs
The first: a clock function subtracted two timestamps in i64 and overflowed near u64::MAX. The second: an RTP parser read the extension length before checking the packet was long enough to have one — short packet, X bit set, straight off the end of the buffer. That's a network packet, from anyone. The third: a 42-byte FLAC header claimed billions of samples, so the decoder tried to allocate 80 GB on a chip with 512 KB of RAM. Bold.
In Rust those were a panic, a bounds check and an abort — on my laptop, in a test, before any hardware existed. In C++ they are undefined behaviour, a heap over-read that cheerfully hands back whatever was sitting next to it, and a null malloc nobody checked. All three silent. Two of them reachable by a stranger with a malformed packet, which is precisely the class of defect NIST's secure software development guidance tells you to design out rather than test out.
Now the embarrassing admission: finding number two was my code. Written carefully. Already passing round-trip tests against ffmpeg. Careful is not a memory model.
The Pinout Belongs In The Compiler, Not A Forum Post
Every ESP32 board has trap pins — strapping pins that decide boot mode, pins wired to the PSRAM bus, a camera bus that eats sixteen of them in one go. In Arduino this is a header file you copy off a forum, and when you get it wrong the board boots into download mode and says absolutely nothing, because a #define has never been wrong about anything in its life.
So the boards became data: every pin, with a cited source, fail-closed. Then it generates Rust.
$ espino pin --board esp32-c6-devkitc-1 8
GPIO8 refused: a strapping pin on esp32-c6-devkitc-1 exit 2
$ espino pin --board esp32-c3-devkitm-1 22
esp32c3 has no GPIO22
The generated file ships a const fn user_gpio that asserts at compile time. A wrong pin becomes a build error at 3pm instead of a mystery at 3am. Same principle as the rest of our stack: make the invalid state unrepresentable rather than documenting it and hoping.
It Runs Before The Board Shows Up
The board is a trait with two implementations. One is the chip. The other is my laptop, with a camera that makes colour bars and a microphone that makes a tone. Same sketch, same MJPEG server, same RTP on the wire:
$ ffmpeg -i http://127.0.0.1:8080/stream -frames:v 30 -f null -
Video: mjpeg (Baseline), yuvj420p, 320x240
frame= 30
CI runs the real sketch for twelve loops and catches every frame and audio block — 12/12 frames, 12/12 PCM blocks, zero lost. A longer run pulled 181 frames over HTTP. Try that with an .ino file. You can't, and it isn't C++'s fault: it's that Arduino welds the program to the board, and nothing encourages you to put a seam in between.
Where Rust On Arduino Still Hurts
I'm not going to pretend embedded Rust is a finished story. It charges you for things Arduino gives away, and if you only ever hear the upside you'll be furious at me by week two.
The Libraries Don't Exist Yet
I needed audio codec chips, so I hand-wrote five of them from Espressif's register tables. Seventy-five clock rows for one chip. That is a genuine nightmare to obtain what #include <ES8311.h> hands you for free in Arduino C++. The ecosystem gap is real, and it's the strongest argument against Rust on Arduino for any project whose part already has a mature library sitting on a shelf.
The Build Is Fat And The Toolchain Is A Personality
One CLI is 250 crates and two minutes. Arduino does a blink in seconds. Xtensa still wants a compiler fork, build-std, and a linker script before it will speak to you. And yes, the borrow checker fought me — four missing 'static lifetimes on peripheral fields. It won all four. It was right all four times, which is annoying in a way I have made peace with.
Numbers, If You Like Numbers
Camera and microphone sketch on a XIAO ESP32-S3: a 1,092,080-byte app image, 26.45% of the partition. Bare-metal filesystem blink: 114,288 bytes, 3.6%, byte-identical to what espflash produces from the same ELF. A pure-Rust LittleFS packer wrote 50 files that Arduino's C mklittlefs unpacked — 50 identical, 0 different — and the same result in reverse. A merged 8 MB flash image matched espflash --merge on all 8,388,608 bytes. Ten packages: 78 suites, 375 tests, 0 failed.
So: if you're wiring a soil sensor for a plant, use Arduino C++. The library exists, the example compiles, and the plant does not care about your memory model. But if the thing has a camera and a microphone and sits in someone's house on their network for five years without an update — because nobody updates the firmware in a doorbell, a point the EFF has been making for years — then a bounds check you didn't have to write is worth more than the week a library saved you.
The argument isn't really "Rust on Arduino." It's this: keep the Arduino promise, change what's underneath it. They were never the same thing. setup() and loop() survive. What changes is that the parser can't run off the end of a packet, the pinout is a compile error instead of a forum post, and the whole toolchain builds without a C compiler anywhere in it.
Ask me again when the LED blinks. The facade is rusty_esp_arduino. The RTOS side of the same argument is Kairos. The doorway from that hardware into the mesh is Janus. The radio is esp32 iroh. If you want the rest of the argument, what digital freedom actually means is this same idea one layer up, the Eras roadmap shows where the hardware lands, and the Freedom Guide is where you start. The rest of the writing lives in Learn.

