Why Async Rust Bloat Is Real — And How to Fix It
If you’ve spent any time writing firmware for resource-constrained microcontrollers, you know the pain: Async Rust never left the MVP state. We were promised zero-cost abstractions, but when you look at the generated MIR, you’re greeted with a bloated state machine that treats every function like a complex, long-running task. It’s time we stop pretending this is "zero-cost" and start addressing the compiler-level inefficiencies that are killing our binary sizes.
The core issue is that the compiler treats every async block as a state machine with mandatory Returned and Panicked states. Even for a trivial function that just returns a constant, the compiler generates a state machine that checks for completion and panics if polled again. This isn't just a minor annoyance; it’s a fundamental design choice that forces unnecessary branching and binary bloat into your firmware.
Here’s where most people get tripped up: they assume LLVM will clean up the mess. We often rely on opt-level=z or s to shrink our binaries, but LLVM is conservative. Because the compiler inserts these panicking branches to satisfy the Future contract, LLVM can’t prove that a future won’t be polled again after completion. It sees a potential side effect—the panic—and keeps the code path alive. You end up with a massive state machine for a function that could have been a simple return value.
If you want to see the impact, look at your generated MIR. You’ll find that even the simplest async block carries the overhead of a CoroutineLayout with multiple states. This is the "MVP" tax. We are paying for safety mechanisms that, in many embedded contexts, are entirely redundant.
To fix this, we need to move beyond workarounds. I’ve been experimenting with compiler-level patches that allow us to opt out of these panicking states. By treating the Panicked state as an optional feature—similar to how we handle integer overflow checks—we can see binary size reductions of 2% to 5% in real-world firmware.
Why does this matter? Because in embedded systems, every byte counts. If you’re working on a project where memory is tight, you shouldn't be forced to choose between the ergonomics of async and the efficiency of manual state management.
Here is how you can start pushing for change:
- Audit your own binary sizes using
cargo-bloatto identify which futures are contributing the most to your footprint. - Stop assuming the compiler knows best; if you see a massive state machine for a simple operation, it’s a sign that the abstraction is leaking.
- Support Project Goals that focus on reducing async bloat rather than just adding new features.
This next part matters more than it looks: we need to stop treating the Future contract as immutable dogma. If we can prove that a future is safe to poll without panicking, the compiler should be smart enough to strip the state machine entirely.
Is it really "zero-cost" if you have to fight the compiler to get the performance you need? I don't think so. We need to demand better optimization passes that recognize when a state machine is unnecessary. If you’re tired of the bloat, start by profiling your own code and pushing for these compiler-level fixes. Try this today and share what you find in the comments, or read our breakdown of embedded Rust optimization techniques next.