· 7 min read ·

Rust's Growing Pains Don't Stop When You Learn the Borrow Checker

Source: rust

The conventional story about Rust goes like this: the borrow checker is brutal, the learning curve is steep, but once you internalize ownership, things click and development flows smoothly. The Rust team’s recent research into developer challenges complicates that story considerably. Beginners do struggle with ownership and lifetimes. But experts don’t graduate to easy mode. They encounter a different set of friction points that are, in some ways, harder to resolve because they sit at the intersection of language design, ecosystem immaturity, and external certification requirements that the core team cannot unilaterally fix.

This is actually a meaningful finding. A language that only frustrates newcomers is a language with an adoption problem. A language that also frustrates experts has a retention problem. The Rust team is now explicitly naming both.

The One Challenge That Doesn’t Go Away

Among all the domain-specific findings, compile times stand out as the only challenge that crosses every cohort. Novices complain about it. Experts complain about it. Embedded developers, web developers, network developers, all of them cite it. The Rust Survey has listed compilation speed as the top pain point every year from 2020 through 2024, with roughly 61% of respondents marking it as significant.

The numbers behind the complaint are concrete. A clean build of a medium-sized Rust project (50,000 to 100,000 lines) commonly runs two to five minutes. Equivalent Go code compiles in seconds. The Rust compiler’s own source, around 1.7 million lines, takes 15 to 25 minutes on typical developer hardware. The gap is not just psychological.

Several partial solutions exist, and they’re worth knowing about. The mold linker is a drop-in replacement for ld and lld that cuts link time by roughly 8x on Linux. Linking is often where the final minutes of a Rust build disappear, and mold addresses that specific bottleneck without touching the compilation front-end. sccache (Mozilla’s shared compilation cache) is standard in CI environments and can back a cache to S3, GCS, or Redis, avoiding redundant compilation of unchanged crates across machines.

The more ambitious solution is the Cranelift codegen backend, an alternative to LLVM for debug builds. Cranelift trades runtime performance for compilation speed, yielding roughly 2x to 5x faster debug builds. It remains nightly-only as of early 2026, not yet stable, but it represents the most significant architectural change to the compilation pipeline in years. Meanwhile, the parallel front-end, which allows the query-based compilation system to execute work concurrently, has been landing in nightly and is being tracked for stabilization.

None of these individually close the gap to Go or Java. They are incremental improvements to a fundamental structural issue: LLVM is a powerful but slow backend, and Rust’s monomorphization strategy generates far more code than most languages.

Async: The Expert Complexity Cluster

For developers building network services, the challenge that replaces the borrow checker is async. The Rust async model is technically sound and zero-overhead in principle, but it exports a great deal of complexity to the user.

The foundational issue is that Rust’s async functions compile to hand-rolled state machines implementing the Future trait. The poll() method returns either Poll::Ready(T) or Poll::Pending. This is efficient, but it requires an executor to drive those futures, and the standard library ships no executor. Tokio fills this role for roughly 70% of async Rust projects, with async-std, smol, and the embedded-focused embassy serving the rest. Library authors face a recurring choice: couple to Tokio, stay runtime-agnostic (which is harder), or publish multiple compatibility layers.

The Pin<P> type, introduced in Rust 1.33, adds another layer. Async state machines can be self-referential (a future holding a reference to its own stack frame), which means they cannot be moved safely in memory. Pin enforces this statically, but it interacts awkwardly with Rust’s ownership model in ways that produce some of the most confusing type errors in the ecosystem.

async fn in traits stabilized in Rust 1.75 (December 2023), which resolved a long-standing ergonomics problem that had required the async-trait procedural macro. That macro boxed every returned future, adding heap allocation overhead. Native support eliminates that cost, but with a caveat: object-safe traits (usable with dyn) require explicit boxing to handle the indeterminate future sizes.

Cancellation remains the deepest unsolved problem. In Rust, dropping a future cancels it silently. There is no structured concurrency primitive comparable to Go’s context.Context or Swift’s TaskGroup. Tokio provides CancellationToken as a library solution, but it is opt-in and invisible to callers who don’t know to look for it. Cancellation bugs in async Rust are subtle and hard to audit.

Safety-Critical Domains: The Certification Gap

For teams building automotive, aviation, or industrial systems, Rust’s technical merits matter less than its certification status. Safety standards like ISO 26262 (automotive, up to ASIL D), DO-178C (avionics), and IEC 61508 (industrial) require that the toolchain itself be qualified, meaning its behavior must be formally characterized and tested.

The major milestone here is Ferrocene, a qualified Rust toolchain developed by Ferrous Systems and AdaCore. Ferrocene 23.06, released in June 2023, became the first Rust toolchain qualified under ISO 26262 at ASIL D (the highest automotive safety integrity level) and IEC 61508 at SIL 4. The Ferrocene Language Specification, which formally characterizes the Rust subset that Ferrocene covers, was later donated to the Rust project, providing an official language specification for the first time.

MISRA Rust, published in 2024, adds 119 coding rules for using Rust in safety-critical automotive contexts. MISRA had previously only addressed C and C++; the existence of a MISRA document for Rust signals that the automotive industry is taking adoption seriously enough to define constraints.

DO-178C certification for avionics remains out of reach. The formal verification requirements are stricter, and qualifying the full rustc compiler (1.7 million lines) is not practical. Research tools like Kani (AWS’s Rust model checker) and Creusot are working toward formal verification of Rust programs, but production-ready DO-178C tooling does not exist yet.

Embedded: Ecosystem Maturity in Progress

Embedded Rust has made substantial progress over the past few years, but ecosystem maturity remains uneven. The no_std model (disabling the standard library for bare-metal targets) is functional, but it means a significant fraction of crates on crates.io are unusable without explicitly supporting no_std behind a feature flag.

embedded-hal 1.0, the Hardware Abstraction Layer trait crate, released in January 2024 after years at 0.2.x. The 1.0 release was a breaking change that required porting the entire ecosystem of device drivers. As of early 2026, most major drivers have been updated, but projects that had stabilized on 0.2.x face migration work, and finding a driver that matches your chip and targets 1.0 rather than 0.2 still requires checking.

Embassy, the async embedded framework, has matured significantly. It provides a zero-allocation async executor that works on STM32, nRF, RP2040 (Raspberry Pi Pico), and ESP32, among others. The async model for embedded is structurally different from Tokio, since there is no heap, no threads, and scheduling works via interrupt-driven waking. Embassy makes this accessible but requires understanding the execution model to avoid subtle priority inversion bugs.

Linker script management, chip-specific memory layout configuration, and debug tooling (particularly compared to mature C ecosystems with decades of ITM/SWO support) remain friction points that embedded developers cite consistently.

The Borrow Checker Is Still Changing

For completeness: the borrow checker itself is not finished. Polonius, a reformulation of the borrow checker using Datalog, accepts a strictly larger set of valid programs than the current NLL (Non-Lexical Lifetimes) implementation. The motivating example is patterns like:

fn get_or_insert<'a>(map: &'a mut HashMap<K, V>, key: K) -> &'a V {
    if let Some(v) = map.get(&key) {
        return v;
    }
    map.insert(key, Default::default());
    map.get(&key).unwrap()
}

Current NLL rejects this with a spurious “value borrowed while mutably borrowed” error. Polonius accepts it, because it can track that the first borrow ends before the mutable borrow begins. The prototype runs on nightly behind a flag, but it was too slow (cubic complexity in some cases) for production use. A rewrite (informally called Polonius v2) is underway, integrated with the next-generation trait solver. No stabilization date has been announced.

What the Research Actually Tells Us

The Rust team’s decision to conduct structured developer research and publish the findings openly reflects something healthy about how the project is run. The findings are not flattering in places. Experts face real friction. Certification gaps are not closing quickly. Compilation times remain a persistent productivity cost.

But the quote that surfaces from the research is instructive: an engineering manager who has adopted Rust for performance says that even if none of the named improvements shipped, they would still use Rust. That reveals the underlying dynamic. The problems are real, and they are being taken seriously, but they are not severe enough to displace what Rust provides that alternatives do not: memory safety without a garbage collector, a type system that encodes hardware constraints at compile time, and a toolchain that treats correctness as a first-class design goal.

The challenges ahead are mostly tractable. Cranelift will eventually stabilize. Polonius v2 will eventually land. The Safety-Critical Rust Consortium is doing real work toward certification. Embedded-hal’s 1.0 stabilization resolved the biggest ecosystem coordination problem. The async story, while genuinely complex, is improving incrementally with each release.

Rust’s problems are now the problems of a maturing systems language. That is not a trivial achievement.

Was this interesting?