background-shape
March Retro, What Rust Earned Its Keep For
March 30, 2022 · 5 min read · by Muhammad Amal programming

TL;DR — One Rust service shipped (well, in staging). Compile times are still painful. The borrow checker did catch real bugs. Worth keeping for specific use cases, not a general-purpose Go replacement. Tomorrow looks like Go for most things, Rust for the 15% that needs it.

End of March. One month after the why Rust post, I’ve shipped one production-shaped Rust service (Axum + sqlx + tracing, deploying to staging via the same GitHub Actions pipeline we use for the Go services). Three weeks ago I thought I’d be hedging more in this retro. Turns out the hedging is the post.

What stuck

Pattern matching + enums. The thing I miss most going back to Go. match on a Result, an Option, or a domain enum is the cleanest control flow I’ve used in any language. I find myself wanting to model state machines as enums everywhere now, and Go just… doesn’t.

The error-handling discipline. Result<T, E> + ? + thiserror + anyhow is a better story than if err != nil. After three weeks the explicit error layer started feeling like a guarantee instead of a chore.

sqlx’s compile-time SQL checking. Caught three real bugs (typo in column name, wrong nullable Option, type mismatch) before they hit the database. The closest a database layer has felt to “the compiler has my back” in any language.

The borrow checker on concurrent code. I had one PR rejected because I tried to share a &mut HashMap across two tasks. In Go I’d have shipped it with a sync.Mutex and held the lock too long. The borrow checker forced me to think about ownership properly upfront. Annoying at the moment; correct in retrospect.

Distroless + cargo-chef = small images. ~22 MB final image, builds in 40 seconds on warm cache. Operationally indistinguishable from our Go services.

What didn’t

Compile times. Still slow. Cold builds in CI: 4 minutes. Cold builds locally: ~3 minutes. cargo check helps but doesn’t get me to Go’s “save file, see compiler output in <1 second” loop. I’ve adjusted by switching context more during builds, but it’s a real productivity hit.

Async lifetimes. Where ownership meets async is the worst part of Rust ergonomically. Closures capturing references, futures that need to be 'static + Send, Pin<Box<dyn Future + Send>> — all of it shows up in async code in ways it doesn’t in sync code. The Tokio team has done excellent work to hide most of this; some still leaks.

Documentation skew. Rust evolves fast. Tutorials from 2020 reference idioms that are deprecated by 2022. The official Book is current; everything else might or might not be. Search results have to be filtered by date.

Editor + LSP performance on large workspaces. rust-analyzer is great but on a 12-crate workspace it’s slow to index and occasionally misleads on completions. VS Code with rust-analyzer is usable, not delightful.

“Just one more .clone() habit. Resorted to cloning more than I’d like. Worked. Probably haven’t shipped anything that I’d genuinely call idiomatic.

What I’d do differently if starting over

Don’t try to learn lifetimes by writing zero-copy parsers. They’re the hardest possible introduction. Write CRUD code with String and Vec for a month first. Lifetimes will start to make sense when you actually need them.

Use cargo-chef from day one. Not day twelve. The Docker iteration loop before I switched was painful enough I almost gave up.

Pick rustls, not OpenSSL. Saves a class of build pain when cross-compiling for musl. Most crates support rustls-tls as a feature.

Read the Tokio book end to end before writing async code. Skipping ahead and copying examples didn’t work; cross-references to ownership and Pin need foundations.

Write the integration tests first. Rust’s compile times reward integration tests; they exercise broad swaths of code without recompiling. Unit-test-per-function the way I do in Go gives a much worse compile-time experience.

Where Rust goes in our stack from here

Pragmatic split:

  • Continue using Go for new services, internal tools, anything CRUD-shaped, anything tier-3 importance.
  • Use Rust for the next service whose perf budget is sub-10 ms p99, or that does heavy shared-state work, or that runs in a cost-sensitive environment (edge, low-spec VMs).
  • Existing Rust components (the one I shipped this month) stay in Rust. Not rewriting.
  • Library code that other languages will consume: if I write a parser, codec, or similar, Rust’s FFI story makes it the right host language.

Concretely: probably 1 Rust service per 5 Go services as a steady-state. Maybe 1 in 3 once compile times improve further.

Things I expect to change in 2022 H2

  • Stable async traits. Currently you need async-trait macro for trait methods that are async. Native support is on the roadmap.
  • Cargo.toml workspace dependencies. Rust 1.64 brings [workspace.dependencies], cleaning up repetition across member crates.
  • Better incremental compile in workspaces. Each Rust release shaves a few more percent off compile time. Pessimistically: still slow at year-end. Optimistically: tolerable.
  • More AI-tooling integration. LSP signal density should improve as model providers focus on Rust-specific training.

What I’d warn the next person about

Five things in order of importance:

  1. Compile times will frustrate you. Plan around them. Editor LSP saves you from waiting on the compiler for most things, but CI feedback is genuinely slow.

  2. The borrow checker is right. When it complains, it has a reason. Argue with it for a day; you’ll lose. Restructure.

  3. The standard library is intentionally minimal. Don’t expect “batteries included” the way Python or Go feels. Every backend service pulls in 20+ crates as a baseline.

  4. Async is a different language. Sync Rust and async Rust have different rules. The boundary is awkward. Pick one for any given module.

  5. The first month is the worst. It does get better. Not as fast as Go gets easier, but real.

End-of-quarter shape

Three months in. January: containerization + microservices extraction. February: Postgres + indexing + CI/CD with GitHub Actions. March: Rust intro. April’s theme: React 18 concurrent rendering and Next.js — pivoting to the frontend side of the stack, where I’ve been deliberately quiet for years.

If you’ve read all 38 posts since January 3, thanks. They’re partly notes to self, partly stake-in-the-ground claims I want to revisit a year from now. Q2 onward I’ll start mixing in some opinion pieces — “what I’d rebuild from scratch knowing what I know now” type things — between the technical deep-dives. See you in April.