Optimizing Rust Async Runtimes for Embedded Microcontrollers

Programming
Date:November 11, 2026
Topic:
Optimizing Rust Async Runtimes for Embedded Microcontrollers
⏱ 2 min read

You're staring at a 32KB flash limit and a deadline. The C code works but threading is a nightmare of callbacks and state machines. You've heard Rust's async story is better — zero-cost abstractions, memory safety, no data races. Then you hit the wall: which runtime? Embassy? RTIC? A custom executor? The wrong choice costs weeks.

Why Standard Async Fails on Bare Metal

Desktop runtimes like Tokio assume an OS: heap allocation, thread pools, epoll/kqueue. Microcontrollers have none of that. No MMU. No scheduler. No dynamic memory if you're doing it right. Every byte and cycle counts. Standard futures pull in massive dependency trees — alloc, std, parking_lot — instantly blowing your flash budget.

⚠️
WarningAvoid any crate that requires 'std' or 'alloc' in embedded contexts. Check Cargo.toml features before adding dependencies.

Embassy: The Pragmatic Choice

Embassy solves this by design. No heap required. Tasks are statically allocated. The executor runs in a single interrupt priority level, using WFE/WFI for sleep. You get async/await syntax with deterministic memory usage. As of 2026, it supports 50+ MCU families via HAL crates and ships 100+ driver crates — sensors, radios, displays, storage.

rust
#[embassy_executor::task]
async fn blink_led(mut led: Output<'static, AnyPin>) {
    loop {
        led.set_high();
        Timer::after_millis(500).await;
        led.set_low();
        Timer::after_millis(500).await;
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    let led = Output::new(p.PA5, Level::Low, Speed::Low);
    spawner.spawn(blink_led(led)).unwrap();
}

When to Choose RTIC Instead

RTIC (Real-Time Interrupt-driven Concurrency) takes a different approach: no executor at all. It uses hardware interrupts as tasks with static priorities. The framework analyzes your code at compile time, guaranteeing zero data races and optimal stack usage. Best for hard real-time systems where you need provable worst-case execution times (WCET).

CriteriaEmbassyRTIC
Programming modelasync/awaitInterrupt handlers + resources
Priority inversionPossible (single priority)Impossible (hardware priorities)
Stack usagePer-task, configurableShared, analyzed statically
Ecosystem depth100+ drivers, growingFewer drivers, mature core
Learning curveLower (familiar async)Steeper (resource model)
"

The best runtime is the one you don't have to think about. Embassy lets you write business logic, not scheduler logic.

— James Munns, Embassy core team

Optimization Checklist for Any Runtime

Regardless of choice, these moves shrink flash and RAM:

toml
# Cargo.toml
[profile.release]
opt-level = "z"        # Optimize for size
lto = true               # Link-time optimization
codegen-units = 1        # Better optimization
panic = "abort"          # No unwinding overhead
strip = true             # Strip symbols

[features]
default = ["defmt", "rt"]
rt = []
💡
TipEnable 'defmt' for logging — it uses compile-time string formatting, zero runtime cost. Pair with probe-rs for JTAG/SWD output.

Use 'embassy-time' for timers instead of busy-wait loops. Leverage DMA for peripheral transfers — the CPU sleeps while data moves. Profile with 'cargo size --release -A' after every major change. Track flash/RAM like a budget.

Start Here This Week

Pick one: Embassy for productivity and ecosystem, RTIC for hard real-time guarantees. Clone the embassy-stm32 or rtic-stm32 template repo. Blink an LED with async. Add a sensor driver from crates.io. Measure flash usage. Ship something. The runtime war ends when you deploy.

Share𝕏 Twitterin LinkedInin Whatsapp