Over the past few years the tools underneath most working developers have been quietly replaced. The bundler compiling your TypeScript, the linter checking your Python, the proxy in front of your API, the microVM your Lambda runs inside - a large share of that layer is now Rust. Not because Rust is fashionable, but because each of those teams hit a specific wall and Rust was the cheapest way through it.
This is a long, practical look at why. What the language actually does, what it is good at, what it costs, who has moved and what they said about it afterwards, and how to adopt it without betting a quarter on a rewrite that does not land.
What Rust is
Rust is a compiled systems language with no garbage collector and no runtime, which puts it in the same performance category as C and C++. What makes it different is that the compiler proves memory safety before your program runs, using a set of rules about ownership rather than a runtime that cleans up after you.
That single design choice is the whole story. It is why Rust can replace C in a kernel driver, replace Go in a latency-sensitive service, and replace JavaScript in a bundler - three problems with nothing in common except that all three want speed without a class of bugs.
Ownership, in one example
Every value has exactly one owner. When the owner goes out of scope, the value is freed. Assigning a value to a new variable moves ownership rather than copying it, and the compiler stops you using the old name afterwards.
let a = String::from("hello");
let b = a; // ownership moves from `a` to `b`
println!("{b}"); // fine
// println!("{a}"); // error[E0382]: borrow of moved value: `a`Borrowing
Moving everything would be unusable, so you can lend a reference instead. The rule the compiler enforces: any number of shared references, or exactly one mutable reference, never both at once.
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
fn main() {
let doc = String::from("the compiler is the pair programmer");
let n = word_count(&doc); // borrowed, not moved
println!("{doc} has {n} words");
}That one-mutable-reference rule looks restrictive until you see what it eliminates. This is iterator invalidation - a bug that in C++ is undefined behaviour and in Python is a runtime surprise:
let mut items = vec![1, 2, 3];
let first = &items[0]; // shared borrow starts here
items.push(4); // error[E0502]: cannot borrow `items` as mutable
// because it is also borrowed as immutable
println!("{first}"); // …because this would read freed memorypush may reallocate the vector's backing buffer, which would leave first dangling. The compiler will not let it happen.Lifetimes are the third piece: annotations that tell the compiler how long a reference stays valid. Most of the time they are inferred and you never write one. When you do have to write them, you are usually doing something that genuinely needs the thought.
What Rust is good at
Memory safety without a garbage collector
The two traditional options were manual memory management, which is fast and dangerous, or a garbage collector, which is safe and introduces pauses. Rust is the first mainstream language to decline both. Use-after-free, double free, buffer overrun, null dereference and data races are compile errors, not incidents.
The industry number that keeps getting cited is that roughly 70% of serious vulnerabilities in large C and C++ codebases are memory-safety bugs. Microsoft has published that figure for its own CVEs; Google has published similar figures for Chrome and Android. Removing that category is not an incremental security improvement - it is most of the problem.
Concurrency the compiler checks
Rust's ownership rules extend to threads through two marker traits: Send (safe to move to another thread) and Sync (safe to share between threads). Types that are not safe to share simply do not compile in that position.
use std::thread;
fn main() {
let mut total = 0;
thread::spawn(|| {
total += 1; // error[E0373]: closure may outlive the current function
}); // it borrows `total`, which lives on the main thread's stack
}use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let total = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let total = Arc::clone(&total);
handles.push(thread::spawn(move || {
*total.lock().unwrap() += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("{}", total.lock().unwrap()); // always 8
}Arc for shared ownership, Mutex for exclusive access. You cannot read the value without taking the lock - the API makes the mistake unrepresentable.For data parallelism, rayon turns a sequential iterator into a parallel one by changing one method call, and the type system guarantees the result is race-free.
use rayon::prelude::*;
let total: u64 = records.iter().map(|r| r.amount).sum(); // sequential
let total: u64 = records.par_iter().map(|r| r.amount).sum(); // parallelErrors are values, and null does not exist
There are no exceptions and no null. A function that can fail returns Result<T, E>; a value that might be absent is Option<T>. Both are ordinary enums, and the compiler will not let you use the success value without handling the other case.
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
let raw = fs::read_to_string(path)?; // `?` returns early on Err
Ok(raw.trim().to_string())
}? operator makes propagation a single character, which is why Rust error handling reads nothing like Go's.Domain errors are usually a custom enum. thiserror generates the boilerplate, and the result is an error type that documents every way an operation can fail.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum OrderError {
#[error("order {0} was not found")]
NotFound(String),
#[error("insufficient stock: {requested} requested, {available} available")]
InsufficientStock { requested: u32, available: u32 },
#[error(transparent)]
Database(#[from] sqlx::Error),
}A type system that models the domain
Rust enums are sum types: a value is exactly one variant, and each variant carries its own data. Combined with exhaustive pattern matching, this makes whole categories of state bug impossible to write.
enum Payment {
Card { last4: String },
BankTransfer { iban: String },
Cash,
}
fn describe(payment: &Payment) -> String {
match payment {
Payment::Card { last4 } => format!("Card ending {last4}"),
Payment::BankTransfer { iban } => format!("Transfer from {iban}"),
Payment::Cash => "Cash".to_string(),
}
}Payment::Crypto variant and every match in the codebase fails to compile until it is handled. That is the refactoring guarantee in one sentence.Predictable performance
No garbage collector means no GC pauses, which matters less for throughput than people assume and enormously for tail latency. Discord's published account of moving a real-time state service from Go to Rust is the canonical example: the average was fine, the p99 spikes caused by garbage collection were not.
Abstractions in Rust are designed to be zero-cost - iterators, generics and traits compile down to roughly what you would have written by hand. You can write high-level code without paying for it at runtime, which is not true of most languages that offer the same expressiveness.
Cargo, and tooling that comes in the box
This is underrated. Coming from C or C++, where the build system is its own career, Cargo is a revelation: one tool for dependencies, building, testing, benchmarking, documentation and publishing, with a lockfile that works and a resolver that does not surprise you.
cargo new my-service # scaffold a project
cargo add axum tokio serde # add dependencies
cargo build --release # optimised build
cargo test # unit, integration and doc tests
cargo fmt # the formatter everyone uses
cargo clippy -- -D warnings # the linter, and it teaches you the language
cargo doc --open # generated docs for your whole dependency treeclippy is worth calling out separately - its suggestions are the fastest way a new Rust developer learns idiomatic code.One binary, anywhere
cargo build --release produces a statically-ish linked binary with no interpreter, no runtime and no dependency on the target machine having anything installed. Cross-compilation is a target triple away. For CLI tools and containers this removes an entire category of deployment problem.
rustup target add aarch64-unknown-linux-musl
cargo build --release --target aarch64-unknown-linux-musl
# resulting container
# FROM scratch
# COPY target/aarch64-unknown-linux-musl/release/my-service /my-service
# ENTRYPOINT ["/my-service"]FROM scratch image containing one file is a meaningfully smaller attack surface than a base image with a package manager in it.It embeds into other languages
This is the mechanism behind most of the adoption you have actually noticed. Rust compiles to a native library that Node.js, Python, Ruby or Go can call, and to WebAssembly for the browser. You do not have to rewrite an application to use Rust - you replace the ten percent of it that is slow.
| Host language | Binding layer | Typical use |
|---|---|---|
| Node.js | napi-rs | Parsers, bundlers, image processing, crypto |
| Python | PyO3 and maturin | Numeric kernels, linters, package resolvers |
| Browser | wasm-bindgen and wasm-pack | Codecs, editors, simulation, CAD |
| Ruby | magnus | Hot paths in Rails applications |
| Anything with a C ABI | extern "C" | The universal fallback |
Why everyone is moving to Rust
Four distinct pressures arrived at roughly the same time, and each one on its own would have moved some teams. Together they moved an industry.
1. The security arithmetic stopped being arguable
Google's Android team began prioritising memory-safe languages for new code around 2019. They did not rewrite the existing C and C++ - they wrote new code in Rust and let the old code age. The published results are the strongest empirical case anyone has made for the language.
| Measure | Reported result |
|---|---|
| Memory-safety share of Android vulnerabilities | 76% in 2019, 24% in 2024, below 20% in the 2025 data |
| Absolute count of memory-safety vulnerabilities | 223 in 2019, under 50 in 2024 |
| Vulnerability density, Rust versus C and C++ code | Google reports roughly a 1000× reduction |
| Rollback rate for Rust changes | Materially lower than the equivalent C++ changes |
| Code review effort | Google reports fewer review hours per Rust change |
Government pressure arrived alongside. CISA, the NSA and the White House Office of the National Cyber Director have all published guidance pushing organisations toward memory-safe languages for new development. For companies selling into regulated or government markets, that turned a technical preference into a procurement question.
2. JavaScript tooling hit the ceiling of JavaScript
Bundlers, linters and formatters are compute-bound batch programs - the worst possible fit for a single-threaded interpreted runtime, and the best possible fit for a compiled language with real parallelism. Once esbuild demonstrated the size of the gap, the rewrite became inevitable.
| Tool | Replaces | Built by |
|---|---|---|
| SWC | Babel | Independent, used by Next.js and others |
| Turbopack | webpack | Vercel - now the default bundler in Next.js 16 |
| Rspack | webpack, API-compatible | ByteDance |
| Oxc - parser, linter, formatter, resolver, minifier | ESLint, Prettier and friends | VoidZero |
| Rolldown | esbuild and Rollup | VoidZero - Vite 8's bundler |
| Biome | ESLint and Prettier | Independent, from the former Rome project |
| Deno | Node.js | Deno Land |
3. Python tooling followed the same path
Astral built Ruff, a linter and formatter, and uv, a package resolver and installer, both in Rust, both reporting speedups of one to two orders of magnitude over the tools they replace. uv went from launch in early 2024 to being the default choice for new Python projects in about two years. OpenAI announced its acquisition of Astral in March 2026.
That is the second time in four months an AI lab bought the company building the fast toolchain for a major language - Anthropic acquired Bun in December 2025. Whatever you think of the consolidation, it tells you those labs regard build and package tooling as infrastructure worth owning, and that the fast versions of that tooling are written in compiled languages.
4. Infrastructure teams found the numbers
| Organisation | What they built | The reason they gave |
|---|---|---|
| Cloudflare | Pingora, an HTTP proxy replacing NGINX | Roughly 70% less CPU and 67% less memory at the same traffic, serving over a trillion requests a day |
| AWS | Firecracker, the microVM behind Lambda and Fargate; Bottlerocket | Isolation and start-up latency without a runtime |
| Discord | A real-time state service moved off Go | Garbage collection pauses appearing in tail latency |
| Microsoft | Windows kernel components, parts of Azure | Around 70% of its annual CVEs were memory-safety bugs |
| Meta | Buck2 build system, source-control tooling | Throughput on a very large monorepo |
| Android system code, Chromium's PNG, JSON and font parsers | Vulnerability prevention at the source | |
| Linux kernel | A second language alongside C, since 6.1 | Memory safety in drivers, where most kernel CVEs live |
| Bun | Bun runtime, replacing Node.js + npm + webpack toolchains | Massive performance gains: faster startup, lower memory, unified toolchain, and reduced infra cost |
Read those reasons again and notice what is missing. Nobody adopted Rust because it is elegant. Every case is a specific measurable problem - GC pauses, CPU cost per request, an accumulating vulnerability backlog, build times - where the team could put a number on the pain before they started.
The pattern underneath all four
In almost every case, Rust replaced a leaf, not a tree. A proxy. A parser. A resolver. One service. A native module inside a Node.js application. The component was small enough to rewrite in a quarter, hot enough that the improvement was visible, and bounded enough that a failed attempt cost one team's time rather than a roadmap.
Adopt Rust where you can already name the number you are trying to move. If you cannot, you are picking a language, not solving a problem.
What Rust is bad at
None of this is fatal and all of it is real. Anyone who tells you Rust's only downside is the learning curve has not shipped much of it.
The learning curve is expensive, and it is front-loaded
A competent engineer coming from TypeScript, Python or Java will be unproductive for weeks and merely slow for a couple of months. The struggle is not syntax - it is that ownership forces you to decide things you never had to decide before. Who owns this? How long does it live? Who is allowed to mutate it? Those are good questions and answering them is real work.
Budget for it explicitly. The failure mode is not that people cannot learn Rust - it is that a team adopts it mid-project, hits the wall during a delivery crunch, and concludes the language is the problem.
Compile times
Rust compiles slowly, for structural reasons rather than fixable ones. Generics are monomorphised, meaning a separate specialised copy is generated per concrete type. The borrow checker and type checker do substantial analysis. LLVM optimisation is not cheap. A medium service can take minutes for a clean release build; a large workspace considerably longer.
- Split large crates into a workspace so a change only rebuilds part of the graph.
- Use a faster linker -
lldormold- which is often the single biggest win on Linux. - Run
cargo checkin your editor loop rather thancargo build. - Use
cargo-nextestfor test runs; it parallelises better than the built-in runner. - Cache the
targetdirectory and the registry in CI, withsccacheif the fleet is shared. - Keep the dependency count honest. Every crate you add is compile time you pay forever.
Async is close to a second language
Synchronous Rust is learnable. Async Rust adds Pin, Send and Sync bounds on futures, lifetime puzzles inside async blocks, and error messages that get long. The classic complaint about coloured functions applies: async and sync code do not compose freely, and a library written for one is not usable from the other.
There is also runtime coupling. Tokio is the de facto standard and most of the ecosystem assumes it, so choosing a different executor narrows your library options sharply. Native async fn in traits landed in Rust 1.75 and removed a lot of the old async-trait friction, but the surface is still meaningfully harder than the equivalent in Go or TypeScript.
Ecosystem gaps that are still real
| Domain | State in 2026 |
|---|---|
| Backend services and APIs | Strong. axum, actix-web, tokio, sqlx, serde are mature and widely deployed. |
| CLI tools | Excellent. clap, single-binary distribution, cross-compilation. |
| Systems, embedded, kernel | The core strength. embassy, embedded-hal, vendor SDKs arriving. |
| WebAssembly | Strong, and one of the best reasons to reach for Rust from a web team. |
| Desktop GUI | Weakest area. Several promising projects, no clear mature default. |
| Machine learning and data science | Rust does the fast layer under Python tooling; it is not where you do the science. |
| Business and CRUD web applications | Workable but slower to build than Rails, Django or a TypeScript stack. |
| Enterprise vendor SDKs | Patchy. Check the specific SaaS APIs you depend on before committing. |
Dependency trees and supply chain
Cargo's ergonomics encourage small focused crates, and the consequence is a deep dependency graph. A modest web service can pull in several hundred transitive crates. The ecosystem's answer is tooling rather than fewer dependencies.
cargo install cargo-audit cargo-deny
cargo audit # known vulnerabilities in your tree
cargo deny check # licences, duplicate versions, banned crates, advisories
cargo tree -d # find duplicated versions bloating the buildcargo deny check in CI on day one. Retrofitting a licence policy onto 400 crates is worse than starting with one.No stable ABI
Rust does not guarantee a stable application binary interface between compiler versions, so you cannot reliably dynamically link Rust libraries built by a different toolchain. In practice everything is statically linked, and any cross-language boundary goes through the C ABI with extern "C". This is fine once you know it, and surprising if you arrive expecting shared libraries to work the way they do in C.
unsafe does not disappear
unsafe blocks let you bypass the borrow checker where you must - FFI, hardware access, data structures the compiler cannot prove correct. Safety then depends on the human. Google's own Android reporting includes a high-severity memory-safety vulnerability in 2025 that came from an unsafe block inside a Rust image parser. The rate is dramatically lower, and it is not zero.
The practical discipline: keep unsafe in a small audited layer with a safe wrapper over it, require a comment justifying every block, and set #![deny(unsafe_op_in_unsafe_fn)] so the boundaries stay explicit.
Hiring, and the honest velocity cost
The Rust talent pool is a fraction of the Go, Java or TypeScript pools, and experienced Rust engineers are expensive. The counter-argument - that Rust engineers are usually strong engineers and that teaching a good developer Rust is a solved problem - is true, and it still costs you three months per person.
The velocity cost is real for a specific kind of work. If the requirements are firm and the problem is well understood, Rust's up-front rigour pays for itself within months. If you are exploring - if the data model changes weekly and half the features get deleted - then a language that lets you write sloppy code fast is genuinely the better tool, and Rust will punish every pivot.
A worked example: a small HTTP service
Concrete enough to run. axum for routing, tokio for the runtime, serde for serialisation - the combination most Rust web services in production are built on.
[package]
name = "notes-api"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4", "serde"] }
thiserror = "2"
chrono = "0.4"
tracing = "0.1"
tracing-subscriber = "0.3"
[dev-dependencies]
tower = { version = "0.5", features = ["util"] }
[profile.release]
lto = true
codegen-units = 1
strip = trueuse serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize)]
pub struct Note {
pub id: Uuid,
pub title: String,
pub body: String,
pub created_at: String,
}
#[derive(Debug, Deserialize)]
pub struct CreateNote {
pub title: String,
pub body: String,
}
impl CreateNote {
pub fn validate(&self) -> Result<(), &'static str> {
if self.title.trim().is_empty() {
return Err("title must not be empty");
}
if self.title.chars().count() > 120 {
return Err("title must be 120 characters or fewer");
}
Ok(())
}
}#[derive(Serialize)] generates the JSON encoder at compile time. No reflection, no runtime cost.use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::{Json, Router};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use uuid::Uuid;
mod models;
use models::{CreateNote, Note};
type Store = Arc<RwLock<HashMap<Uuid, Note>>>;
#[derive(serde::Serialize)]
struct ApiError {
message: String,
}
fn fail(status: StatusCode, message: &str) -> Response {
(status, Json(ApiError { message: message.to_string() })).into_response()
}
async fn health() -> &'static str {
"ok"
}
async fn list_notes(State(store): State<Store>) -> Response {
let notes: Vec<Note> = store.read().unwrap().values().cloned().collect();
Json(notes).into_response()
}
async fn get_note(State(store): State<Store>, Path(id): Path<Uuid>) -> Response {
let notes = store.read().unwrap();
match notes.get(&id) {
Some(note) => Json(note.clone()).into_response(),
None => fail(StatusCode::NOT_FOUND, "note not found"),
}
}
async fn create_note(
State(store): State<Store>,
Json(input): Json<CreateNote>,
) -> Response {
if let Err(message) = input.validate() {
return fail(StatusCode::UNPROCESSABLE_ENTITY, message);
}
let note = Note {
id: Uuid::new_v4(),
title: input.title.trim().to_string(),
body: input.body,
created_at: chrono::Utc::now().to_rfc3339(),
};
store.write().unwrap().insert(note.id, note.clone());
(StatusCode::CREATED, Json(note)).into_response()
}
pub fn app(store: Store) -> Router {
Router::new()
.route("/health", get(health))
.route("/notes", get(list_notes).post(create_note))
.route("/notes/{id}", get(get_note))
.with_state(store)
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let store: Store = Arc::new(RwLock::new(HashMap::new()));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
tracing::info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app(store)).await.unwrap();
}{id} braces - axum 0.8 changed this from the older :id syntax, and the old form now panics rather than failing silently.Note what the extractors are doing. Json<CreateNote> means a malformed body is rejected before your handler runs, and Path<Uuid> means an ID that is not a valid UUID never reaches your code. The type signature is the validation.
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt; // brings `oneshot` into scope
#[tokio::test]
async fn rejects_an_empty_title() {
let store = Arc::new(RwLock::new(HashMap::new()));
let response = app(store)
.oneshot(
Request::builder()
.method("POST")
.uri("/notes")
.header("content-type", "application/json")
.body(Body::from(r#"{"title":"","body":"x"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
}tower::Service, so you call it directly.The pattern most teams should actually use
You almost certainly should not rewrite your API in Rust. What you might do is move one hot function into it and call that function from the stack you already have. napi-rs makes this genuinely straightforward from Node.js.
use napi_derive::napi;
#[napi]
pub fn normalise_phone(input: String, country: String) -> Option<String> {
let digits: String = input.chars().filter(|c| c.is_ascii_digit()).collect();
match country.as_str() {
"BD" if digits.len() == 11 && digits.starts_with('0') => {
Some(format!("+880{}", &digits[1..]))
}
"BD" if digits.len() == 13 && digits.starts_with("880") => {
Some(format!("+{digits}"))
}
_ => None,
}
}import { normalisePhone } from '@acme/phone'
const e164 = normalisePhone('01712345678', 'BD')
// '+8801712345678' - TypeScript types are generated from the Rust signaturenapi-rs emits the .d.ts from the Rust function signature, so the boundary stays typed on both sides.The economics of this are much better than a rewrite. The blast radius is one package. The rollback is reverting to the JavaScript implementation. And you find out whether your team likes writing Rust before anything important depends on the answer.
Rust against the alternatives
| Language | Rust wins on | It wins on | Choose it over Rust when |
|---|---|---|---|
| C | Memory safety, tooling, dependency management | Ubiquity, compile speed, every platform ever made | You need a compiler for hardware Rust does not target |
| C++ | Safety guarantees, build system, ergonomics | Ecosystem depth, hiring pool, existing codebase gravity | You have millions of lines of C++ and no reason to leave |
| Go | Performance, no GC pauses, type expressiveness | Learning curve, compile speed, hiring, time to first ship | You need a productive networked service and can tolerate a GC |
| Zig | Maturity, ecosystem size, memory safety guarantees | Simplicity, compile speed, C interop, no borrow checker | You want manual control and a small language - Bun's choice |
| TypeScript on Node or Bun | Raw speed, memory footprint, correctness guarantees | Iteration speed, ecosystem, hiring, shared frontend language | The work is product-facing and the bottleneck is not CPU |
| Java or C# | Memory footprint, start-up time, no GC tuning | Ecosystem, enterprise integration, developer availability | You are inside an existing JVM or .NET organisation |
| Python | Everything performance-related | Everything speed-of-thought-related, plus the ML ecosystem | You are exploring, analysing, or gluing systems together |
Getting started properly
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup update stable
rustc --version
rustup component add clippy rustfmt rust-analyzer
rustup target add wasm32-unknown-unknown # if you want WebAssemblyrustup manages toolchains, components and targets. Pin the toolchain per project with a rust-toolchain.toml file.[workspace]
resolver = "3"
members = ["crates/*"]
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }- The Rust Programming Language - the official book. Genuinely the best starting point; work through it rather than skimming.
- Rust by Example - for when you want the syntax without the prose.
- Rustlings - small compile-error exercises, which is exactly how you learn the borrow checker.
cargo clippy- treat every lint as a lesson for the first few months.- This Week in Rust - the ecosystem newsletter worth the subscription.
A sane CI baseline
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo deny check
cargo build --releaseHow to adopt it without betting the company
- Find a bounded, measurable problem. A hot endpoint, a batch job, a parser, a CLI tool. Write down the number you expect to move before you start.
- Pick a component whose interface is already stable, so you are porting behaviour rather than designing it.
- Give one or two engineers real time to learn - weeks, not evenings. Rust punishes half-attention more than most languages.
- Build it behind the same interface as the thing it replaces, so switching back is a configuration change.
- Run both implementations in parallel against production traffic before you cut over, comparing outputs as well as latency.
- Measure the thing you wrote down in step one. If it did not move, say so and stop.
- Set up the tooling properly the first time:
clippydenying warnings,cargo denyin CI, a pinned toolchain, cached builds. - Write down your conventions early - error types,
unsafepolicy, async runtime, logging. Rust has fewer defaults than a framework does. - Only after all of that, consider a second component. Adoption should be earned per project, not announced once.
When to reach for Rust, and when not to
Reach for it when
- Tail latency matters and garbage collection pauses are showing up in your p99.
- The component is CPU-bound and running constantly, where a 3× improvement is a real infrastructure bill.
- You are writing something that parses untrusted input - a decoder, a protocol implementation, a file format.
- Memory safety is a compliance or security requirement rather than a preference.
- You need a single binary with no runtime - CLI tools, agents, embedded, edge.
- You are targeting WebAssembly and want performance rather than convenience.
- The requirements are stable and the component will outlive several product pivots.
Do not reach for it when
- The product is still being discovered and the data model changes every sprint.
- The bottleneck is I/O, a database query, or a network call - Rust will not fix any of those.
- Your team has no Rust experience and no room in the schedule to build some.
- You need a mature SDK for a vendor that only ships Python, TypeScript and Java clients.
- It is a CRUD application. A boring stack will ship it in a third of the time.
- You are building a desktop GUI and want the ecosystem to have solved your problems already.
- The honest reason is that the team wants to learn Rust. That is a fine reason for a side project and a bad one for a delivery commitment.
Verdict
Rust is not overhyped, and it is not the default answer either. It solved a problem that had been considered a permanent trade-off - safety or speed, pick one - and the companies that adopted it early published numbers rather than opinions. That is why the adoption curve looks the way it does.
What it charges for that is up-front thinking. Ownership, lifetimes, error types, async bounds: the compiler makes you resolve questions that other languages let you defer until an incident. If your problem is worth that rigour, you get software that is fast, small, and does not wake anyone up. If your problem is that you are not sure what you are building yet, the same rigour is a tax on every change of direction.
The teams getting it right are almost never the ones that picked Rust as a company language. They are the ones who found one component where the numbers justified it, rewrote that, measured, and then did it again. Your bundler, your linter, your proxy and your package manager all arrived in your stack that way, and most of you never noticed.
Start with one leaf. Pick the thing you can already put a number on.
Sources
- The Rust Programming Language and the book - language reference and learning path
- Rust release blog - six-week release cadence, editions, current stable
- Rust in Android: move fast and fix things - Google's 2025 vulnerability and productivity data
- Rust Is Eating JavaScript - a maintained index of Rust-based JavaScript tooling
- Oxc and Rolldown - the VoidZero toolchain behind Vite 8
- uv - Astral's account of rebuilding Python packaging in Rust
- Pingora - Cloudflare's Rust HTTP proxy, open sourced
- Firecracker - the microVM behind AWS Lambda and Fargate
- axum and tokio - the web and async foundations used in the example



