Why Rust Is Growing Fast in Backend Engineering
TL;DR — Rust is hitting backend teams not because it’s trendy but because the cost of memory safety bugs finally exceeds the cost of learning the borrow checker / The 2023 ecosystem (tokio, axum, sqlx, serde) is finally boring enough to deploy without heroics / The real win isn’t speed — it’s the bugs you never write
I’ve been writing backend services for a decade. Most of that was Go, some Java before that, a long stretch of Node when the team needed to ship fast. Two years ago I started porting a payment reconciliation worker to Rust because it kept OOM-killing under load and the GC pauses were starting to show up in our p99 dashboards. I expected to hate the experience. I didn’t. The team rewrote three more services in the following year.
This isn’t a Rust-evangelism post. There are real reasons not to use it, and I’ll get to those. But the question I keep getting from peers — “is Rust actually worth learning in 2023, or is this hype” — has a clear answer now, and the answer is yes, with caveats. The ecosystem crossed a maturity threshold somewhere around the tokio 1.x and axum 0.6 era, and the tooling is finally good enough that you can hire mid-level engineers and have them productive in a quarter rather than half a year.
What follows is what changed, why it matters for backend work specifically, and what to actually evaluate before you propose Rust to your team.
What changed in 2023
The Rust 1.70 release in June stabilized OnceCell and OnceLock in std, which sounds tiny but eliminates one of the most common reasons people reached for lazy_static or once_cell as a dependency. The 1.71 release in July 2023 stabilized Option::is_some_and and improved C-unwind ABI handling. None of these are headline features — they’re the kind of papercuts that, when removed, make the language feel finished.
# Cargo.toml — a typical Jul 2023 backend stack
[package]
name = "recon-worker"
version = "0.3.0"
edition = "2021"
rust-version = "1.71"
[dependencies]
tokio = { version = "1.29", features = ["full"] }
axum = "0.6"
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "uuid", "chrono"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
thiserror = "1.0"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
A year ago this Cargo.toml would have had pinned commits, RFC-stage crates, and at least one fork of something. Today it’s all stable 1.x releases. sqlx 0.7 landed in late June with proper async migrations and better connection pool tuning. tokio 1.29 finished the cooperative scheduling work that makes long-running tasks behave under load. axum 0.6 is the version where the extractor ergonomics finally clicked.
The actual backend wins
People talk about Rust like it’s a performance language. It is, but that’s not why backend teams are picking it up. The wins I’ve measured on real services:
Memory predictability. Our reconciliation worker had a 1.2 GB resident set in Go and could spike to 3+ GB under burst load. Rewritten in Rust it sits at 110 MB and the peak is bounded by the configured channel sizes. No GC, no GOGC tuning, no surprise pause when the heap doubles.
Error handling that survives refactors. Result<T, E> plus ? plus thiserror means the compiler tells you when you forgot to handle a new failure mode. Every Go service I’ve worked on has at least one if err != nil { return err } chain where someone dropped context. Rust makes that hard to do accidentally.
Latency tail behavior. Without a GC, your p99.9 is a function of contention and IO, not heap state. For services that talk to four downstream APIs and a database per request, removing GC variance from the equation makes capacity planning honest.
Refactoring confidence. The type system catches the class of bugs that you’d normally find via integration tests or in production. I’ve done multi-thousand-line refactors of Rust services where the only test failures were genuine semantic changes I’d made on purpose.
The thing I didn’t expect: code review is faster. Most of what you’d flag in a Go or Python review (forgotten error, nil deref, race condition, unhandled context cancellation) is rejected by the compiler before the PR opens.
Where the friction still lives
It would be dishonest to pretend this is free. The borrow checker has a learning curve, and the curve is steeper for engineers coming from GC’d languages than for those with C++ backgrounds.
Compile times remain the single biggest day-to-day complaint. A clean release build of a moderately sized axum service takes 90 seconds on an M2 Pro. Incremental dev builds are 3-8 seconds, which is workable but worse than go build on equivalent code. Mitigations exist:
# ~/.cargo/config.toml — speed up dev builds
[build]
rustc-wrapper = "sccache"
[profile.dev]
opt-level = 0
debug = 1 # less debug info, faster link
[profile.dev.package."*"]
opt-level = 2 # but optimize deps once
# faster linker on macOS
[target.x86_64-apple-darwin]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=/opt/homebrew/bin/zld"]
Async Rust has rough edges. Pinning, Send bounds on trait methods, async fn in traits requiring async-trait — these are real papercuts. The 2023 status is “very productive once you’re past them, painful while you’re learning them.” If your team is new to Rust, do a sync service first.
Hiring is harder. The pool is smaller than Go or Python. The flip side is that the candidates you do find tend to be self-selected for caring about correctness, which has been a positive bias for us.
Async runtime fragmentation
The tokio vs async-std debate is essentially over — tokio won. But you’ll still find crates that pull in incompatible runtime assumptions. Pin to tokio early and reject deps that don’t support it. Check tokio.rs for the runtime docs before adopting any async crate.
Common Pitfalls
A few things I’ve watched teams trip on in their first six months:
- Reaching for
Arc<Mutex<T>>reflexively. Most of the time you can restructure to pass ownership instead. When you genuinely need shared mutable state,tokio::sync::RwLockis usually a better default thanstd::sync::Mutexin async code. - Using
unwrap()in production paths. The compiler lets you, but everyunwrapis a panic waiting for the right input. Useexpect("reason this can't fail")if you must, and treat the reason as a code review checkpoint. - Mixing blocking IO into async tasks. A single
std::fs::read_to_stringon a tokio worker thread can stall every concurrent request. Usetokio::fsorspawn_blocking. - Premature trait abstractions. Rust’s traits are powerful enough that engineers from OO backgrounds tend to overuse them. Concrete types until the second caller appears.
- Ignoring
clippy. Run it in CI from day one. The default lints catch real bugs, not just style.
// Anti-pattern: blocking in async context
async fn load_config() -> Config {
let bytes = std::fs::read("config.toml").unwrap(); // BLOCKS the runtime
toml::from_slice(&bytes).unwrap()
}
// Better
async fn load_config() -> anyhow::Result<Config> {
let bytes = tokio::fs::read("config.toml").await?;
Ok(toml::from_slice(&bytes)?)
}
When not to use Rust
I want to be specific here because the wrong-shaped projects burn teams:
- You’re prototyping and might throw it away. Python or TypeScript will get you there in a quarter of the time.
- Most of your work is glue code over HTTP APIs and a database, with no real performance constraints. Go is a better fit. The Rust win in that shape of work is marginal and the tax is real.
- Your team is two engineers and you need to ship a product in six weeks. The borrow checker tax during onboarding will hurt you.
- You need a rich UI rendering pipeline. The Rust web frontend story (Yew, Leptos) is interesting but not production-default in mid-2023.
Use Rust when you have one or more of: hard latency requirements, memory-constrained environments, long-running services where GC tail latency hurts, security boundaries where memory safety is genuinely critical, or a small core of code that gets called billions of times.
Wrapping Up
The honest answer to “should I learn Rust in 2023” is: if you’re a backend engineer planning to still be doing this work in five years, yes. The ecosystem has crossed the line from “exciting” to “boring,” which is exactly when languages become safe to bet on. The tooling around it — see the Rust book for the canonical learning path — is mature enough that the time investment is bounded.
The rest of this month I’ll be writing about the parts of Rust that took me the longest to get comfortable with: ownership, async with tokio, error handling, and shipping secure CLIs and HTTP services. If you’re evaluating Rust for a specific backend problem, those should give you enough surface area to make a real call.