Rust
Rust is a compiled systems language that frees memory without a garbage collector by checking, at compile time, who owns each value and who may borrow it. That check is why safe Rust has no use-after-free and no data races — and why newcomers spend their first weeks arguing with the compiler. The page opens with the twelve compiler errors everyone meets first and the rule behind each, then covers ownership and borrowing, what the compiler cannot check, threads and async, errors and panics, releases and editions up to Rust 1.99, five documented cases, and where Rust is the wrong tool.
- Current version
- Rust 1.99 · 1 Oct 2026
- Memory
- Ownership, checked at compile time
- Releases
- Every six weeks; an edition every three years
Twelve compiler errors everyone meets first, and the rule behind each
Most languages let you write the program and find the memory bugs later — in production, or under a debugger. Rust moves that moment to compile time. The borrow checker, the part of the compiler that tracks who owns each value and who is borrowing it, refuses to build code that could use freed memory or race on shared data. So a newcomer’s first weeks are a series of error messages, and every one of them names a rule rather than a bug. Here are the twelve almost everyone meets, each with its code, the rule it enforces and the usual fix. Every code has a full explanation at rustc --explain E0382 and its siblings. Later sections cite the rows by number.
| # | Error | The rule it enforces | The usual fix |
|---|---|---|---|
| 1 | E0382 · use of a moved value | Assigning a String, a Vec or a Box moves it; the old name is dead | Borrow it with &, .clone() it, or restructure so one owner is enough |
| 2 | E0384 · cannot assign twice to an immutable variable | Variables are immutable unless declared mut | let mut, or shadow the name with a new let |
| 3 | E0596 · cannot borrow as mutable | Changing a value through a reference needs a mut binding and a &mut | Declare it mut; take &mut self in the method |
| 4 | E0499 · cannot borrow as mutable more than once | At most one &mut to a value at a time | Finish with one before taking the next; borrow separate fields of a struct |
| 5 | E0502 · cannot borrow as mutable because it is also borrowed as immutable | Readers or one writer — never both at once | Copy out what you read, then write; or let the read borrow end first |
| 6 | E0505 · cannot move out because it is borrowed | A value cannot leave while someone holds a reference to it | Let the borrow end first, or clone |
| 7 | E0507 · cannot move out of a value behind a reference | A borrower may look, not take | .clone(), std::mem::take, or Option::take() |
| 8 | E0515 · cannot return a reference to a local variable | A reference may not outlive what it points to | Return the owned value — String, not &str |
| 9 | E0597 · borrowed value does not live long enough | The value is dropped while a reference to it is still in use | Move the value to an outer scope, or own it |
| 10 | E0106 · missing lifetime specifier | A returned reference must say which input it borrows from | Name the lifetime — fn f<'a>(x: &'a str) -> &'a str — or return an owned value |
| 11 | E0373 · closure may outlive the current function | A thread may run longer than the function that started it | move the data into the closure; std::thread::scope for borrowed data |
| 12 | E0277 · Rc<…> cannot be sent between threads safely | Only Send types may move to another thread, and only Sync types may be shared; Rc is neither | Arc for shared ownership; a Mutex or atomics for shared change |
Ownership: one owner, borrowed references, and no collector
Every language has to answer one question about memory: when is it safe to free a value? A garbage collectorfrees memory nobody usesThe part of a runtime that finds objects nothing refers to any more and frees their memory, so the program never frees it by hand. The price is extra memory and work the program does not schedule — pauses, short or long depending on the collector.Full article →All terms — in Java, Go, Python — answers at run time by finding values nothing points to any more, and pays in CPUCentral Processing Unit — the processorThe processor that executes a program’s instructions. Calling a task “CPU-bound” means it is limited by computation rather than by waiting on disk or network — and the two need opposite fixes, so the distinction is worth making early.All terms and pauses. C and C++ leave the answer to the programmer, and pay in use-after-free and double-free bugs. Rust answers at compile time, with three rules. Each value has exactly one owner — a variable, a field, a collection. There is one owner at a time; assigning or passing the value moves ownership. And when the owner goes out of scope, the value is dropped: its memory is freed, its file closed, its lock released, at a point you can see in the source.
A move is cheaper than it sounds. A String is three words on the stack — a pointer to its bytes on the heap, a length and a capacity. let b = a; copies those three words and marks a as unusable; the bytes on the heap are not touched. That is why row 1 exists: if a stayed usable, two owners would free the same bytes. Small values that own no heap memory — integers, bool, char — implement Copy and are copied instead of moved, so the rule never bites them. .clone() makes a real, deep copy; it is the honest fix for row 1 when you need two independent values, and an expensive habit when it only silences the compiler.
Borrowing is how a value is used without being moved. &a is a shared reference: any number may exist, and none may change the value. &mut a is a mutable reference: only one may exist, and while it does, nothing else may even read the value. That single rule — many readers or one writer — is what rows 2–5 enforce, and it is the same rule a reader-writer lock enforces at run time. Rust checks it at compile time, for every reference, everywhere. It is also why an iterator in Rust cannot be invalidated: the loop holds a shared borrow of the vector, so a push that could reallocate it under the loop does not compile.
A borrow lasts from where it is created to its last use, not to the end of the block. Since the 2018 edition the compiler works this out from the code’s actual flow — the change was called non-lexical lifetimes — which is why code that looks as if it holds two borrows often compiles: the first one had already finished. Lifetimes, the 'a annotations of row 10, are not a way to make a value live longer. They are labels that tell the compiler which input a returned reference came from, so it can check that the caller does not outlive it. Most functions never need one: three elision rules let the compiler infer them, and it asks only when, for example, a function takes two references and returns one.
What the compiler cannot check
The borrow checker proves a narrow, valuable claim: in safe Rust no reference points to freed memory, and no two threads write the same memory without synchronisation. It does not prove the program correct, and it does not prove everything that sounds like memory safety. Reading the guarantee too widely is the most expensive misunderstanding about Rust, so here is the line, drawn exactly.
| Bug | In safe Rust | Why |
|---|---|---|
| Use after free, double free, dangling pointer | Impossible | Ownership and borrowing (rows 1–10) |
| Data race — two threads writing one variable | Impossible | Send and Sync (rows 11–12) |
| Reading past the end of an array | Stopped at run time with a panic | Every index is bounds-checked |
| Integer overflow | A panic in debug builds; silent wrap-around in release builds | Checks are off in release unless overflow-checks = true |
| Memory leak | Possible, and considered safe | An Rc cycle, mem::forget, Box::leak |
| Deadlock | Possible | Two threads taking two Mutexes in opposite order |
| Race condition in logic — check, then act | Possible | Each step is synchronised; the pair is not |
A panic — unwrap(), expect(), an index | Possible; it stops the thread | A panic is safe; it is not handled |
| Logic errors | Possible | The type system checks memory, not intent |
unsafe is the boundary. Inside an unsafe block you may do five things safe Rust forbids: dereference a raw pointer; call an unsafe function, which every C function reached through FFI is; read or write a mutable static; implement an unsafe trait such as Send; and read a union’s fields. Everything else is still checked — the borrow checker keeps running inside unsafe. The block is a promise, written by a person, that invariants the compiler cannot see do hold. The standard library is built on such promises — Vec, Mutex and Arc all contain unsafe — and wraps them in safe APIApplication Programming Interface — contract between programsThe rules one program follows to call another: which requests it may send, in what shape, and what comes back. To the caller it is a contract — everything behind it stays a black box, which is exactly what lets the other side be rewritten without breaking you.Full article →All terms, so each promise is checked once, by the people who wrote it, rather than at every call.
This is why one bug in an unsafe block can surface anywhere: a safe function that trusts it can hand out a dangling reference, and the crash appears far from its cause. Two practices keep the boundary honest. Keep unsafe small and inside a module whose public API is safe, and write the invariant above each block in a // SAFETY: comment, which Clippy can require. And run the tests under Miri — cargo +nightly miri test — an interpreter that detects undefined behaviourwhat the spec leaves openBehaviour a language or specification deliberately leaves undefined: the program may crash, appear to work or do something else entirely, and the outcome can change with the compiler version or the platform. In C, C++ and unsafe Rust it is what a memory bug turns into, because the compiler is allowed to assume it never happens.All terms in unsafe code that an ordinary run sails past. Android’s data is a useful calibration: about 4% of its Rust is inside unsafe blocks, and that code has not turned out more bug-prone than C — the scrutiny the keyword attracts seems to pay.
Threads and async: Send, Sync, and the runtime Rust does not ship
Rows 11 and 12 extend the same rules to threads. A thread may outlive the function that spawned it, so a closure given to thread::spawn must own its data — move — or borrow it inside std::thread::scope, which guarantees every thread ends before the borrowed data does. And two marker traits decide what may cross a thread boundary at all. A type is Send if it can be moved to another thread, and Sync if a shared reference to it can be. The compiler derives both automatically from a type’s fields, so a struct holding an Rc is neither, and the error appears at the spawn, not in production. For a generic container the answer depends on what it holds; the table assumes plain, thread-safe contents.
| Type | Send | Sync | Use it for |
|---|---|---|---|
i32, String, Vec<T> | Yes | Yes | Plain data |
Rc<T> | No | No | Shared ownership on one thread — its count is not atomic |
Arc<T> | Yes | Yes | Shared ownership across threads; the count is atomic |
RefCell<T> | Yes | No | Change through & on one thread, checked at run time |
Mutex<T> | Yes | Yes | Change through & across threads, one at a time |
std::sync::MutexGuard | No | Yes | Holding the lock — it must be released on the thread that took it |
The result is the property the Rust community calls fearless concurrency: a data racetwo threads, one variable, no lockTwo threads or goroutines accessing the same memory at the same time, at least one of them writing, with nothing to order the accesses. The result is undefined — a wrong value, a torn read or a crash much later — and unlike a race condition in logic it can be ruled out mechanically: by a race detector or, in Rust, by the compiler.Full article →All terms is a compile error. It does not make concurrent programs correct — deadlocks and check-then-act races from the table above are still yours — but it removes the class of bug that took Uber six months and a race detector to count in its Go code.
Async Rust: futures, Tokio and cancellation
An async fn in Rust does nothing when it is called. It returns a future — a value describing the work — and the work runs only when something polls it. The language ships the syntax and the Future trait but no runtime to do the polling; you choose one, and for servers that in practice means Tokio, which brings a multi-threaded scheduler that steals work between threads, timers and non-blocking network I/O. Axum, Hyper, Tonic and most of the server ecosystem are built on it. Cloudflare’s Pingora proxy is too, chosen, Cloudflare wrote, because work stealing avoids the problems of a request pinned to one process.
Two traps follow from that design. A blocking call inside an async task — reading a file with std::fs, a CPU-heavy loop, a synchronous database driver — holds a runtime thread, and every other task scheduled on it waits: the same failure as requests.get inside asyncio in Python. Move such work to tokio::task::spawn_blocking. And a task on a multi-threaded runtime may move to another thread at every .await, so everything it holds across an .await must be Send. Holding a std::sync::MutexGuard across one produces "future cannot be sent between threads safely" — row 12 in async form. Release the lock before awaiting, or use tokio::sync::Mutex when the lock genuinely must be held across one.
Errors and panics: Result, the ? operator, and unwrap
Rust has no exceptions and no null. A function that can fail returns Result<T, E> — either Ok(value) or Err(error) — and a value that may be absent is an Option<T>: Some(value) or None. The compiler will not let you use the value without deciding what happens in the other case, and a match must cover every case the type has. The ? operator makes that cheap to write: let user = load(id)?; returns the error to the caller if there is one, carries on with the value if not, and converts the error type on the way.
Libraries usually define their own error enum and let thiserror derive the boilerplate; applications usually collect errors into anyhow::Error and add context at each layer — .context("loading the feature file")? — the same idea as wrapping with %w in Go. What ? cannot do is decide. Somewhere near the edge of the program a match or a logged error turns a failure into a status code, a retry or a fallback, and that place has to exist.
A panic is the other path. unwrap() and expect() turn an Err or a None into a panic; so do an index out of bounds, a RefCell borrowed twice, and integer overflow in a debug build. A panic unwinds the thread — dropping everything it owned, so no memory leaks and no lock stays held — and in a server it usually ends the worker or the request. It is memory-safe. It is not error handling. An unwrap() is a claim, written into the code, that the failure cannot happen, and whoever reviews it should ask what happens when the claim is wrong.
Releases, editions, Cargo, and which Rust you are looking at
Rust 1.0 shipped on 15 May 2015, with a promise that stable code keeps compiling. A new stable version follows every six weeks — Rust 1.99.0 came out on 1 October 2026 — built from the nightly channel through a six-week beta. Changes that would break existing code arrive only through editions, which each crate opts into in Cargo.toml, and crates of different editions link together freely. An edition can change what code means — keywords, defaults, lints — but the same compiler supports every edition indefinitely.
| Edition | Compiler | What changed that you will meet |
|---|---|---|
| 2015 | 1.0, May 2015 | The original language |
| 2018 | 1.31, Dec 2018 | async and dyn become keywords; a simpler module system; non-lexical lifetimes |
| 2021 | 1.56, Oct 2021 | Closures capture individual fields; TryFrom and TryInto in the prelude; arrays iterate by value |
| 2024 | 1.85, Feb 2025 | unsafe extern blocks; gen reserved; if let temporaries dropped earlier; references to static mut denied |
Cargo is the build tool, the package manager and the test runner in one — cargo build, cargo test, cargo doc — with rustfmt for formatting and Clippy for several hundred lints. Packages, called crates, come from crates.io, and Cargo.lock pins the exact versions an application ships. Two habits pay for themselves. cargo audit or cargo deny checks the lockfileexact versions of every dependencyA file the package manager writes next to the list of dependencies, recording the exact version of every package installed — including the ones your dependencies pulled in. Committed to the repository, it makes every install identical; without it, two builds of the same commit can run different code.All terms against the RustSec advisory database in CI/CDContinuous Integration / Continuous Delivery — automatic build, test and deliveryEvery commit is built and put through the tests automatically (CI), and a build that passes travels to the servers the same way (CD). The point is not automation for its own sake: it is that a breakage shows up in minutes instead of on release day.Full article →All terms. And a rust-version field in Cargo.toml states the oldest compiler a crate supports — its MSRV — which the dependency resolver of the 2024 edition respects when it picks versions.
Five cases, and what each one shows
Each case is documented by the people it happened to — engineering blogs, the project’s own security advisory, a platform’s published data. Two show what Rust buys, two show what it does not, and one shows where the gain really comes from.
| Case | What happened | What it shows |
|---|---|---|
| Discord, Feb 2020 | Read States moved from Go to Rust; the two-minute GC spikes disappeared | Memory freed at a known point — no collector to pause |
| Cloudflare Pingora, Sep 2022 | A replacement for Nginx in Rust: about 70% less CPU and 67% less memory | The gain came from the architecture as much as from the language |
| CVE-2024-24576, Apr 2024 | The standard library escaped arguments to Windows batch files wrongly | Safe Rust does not prevent logic bugs, even in std |
| Android, 2019–2025 | Memory-safety bugs fell from 76% of vulnerabilities to under 20% as new code moved to Rust | New code matters more than rewriting old code |
| Cloudflare, 18 Nov 2025 | An unwrap() on an oversized configuration file panicked the proxy fleet | A panic is safe, not handled |
The first case is the Go page’s Discord story from the other side. With no garbage collector, the Rust version of Read States freed each evicted cache entry at the moment it was evicted, so there was no periodic walk over tens of millions of live entries and no spike every two minutes. Discord reported that latency, CPU and memory were all better, with average response times "now measured in microseconds". Two details keep the case honest. The Go service ran on Go 1.9, and later Go releases improved the collector considerably; and Discord had to use nightly Rust for async at the time, which they said themselves. The lesson is narrow and real: for a huge, long-lived, pointer-heavy heap, freeing memory at a known point beats any tracing collector.
The second case is a correction to how the first is usually told. In September 2022 Cloudflare described Pingora, its own HTTPHyperText Transfer Protocol — the protocol of the webThe protocol browsers and servers talk in: the client sends a method and an address, the server answers with a status code and a body. It keeps no memory between requests — every request stands alone, which is why anything that must be remembered is carried explicitly.All terms proxy in Rust, which had replaced Nginx for traffic to origin servers and served over a trillion requests a day. It used about 70% less CPU and 67% less memory than the old service for the same traffic. The post is careful about why. Part of the saving came from Rust replacing Lua; a large part came from the design — one multi-threaded process on Tokio sharing one connection pool, instead of a pool per Nginx worker process — which cut new connections to origin servers to a third. The memory-safety point is separate and simpler: after "a few hundred trillion requests", Pingora had "yet to crash due to our service code".
The fourth case is the strongest evidence for Rust, and it is about new code, not rewrites. Google’s Android team moved new low-level code to memory-safe languages from around 2019 and left the existing C and C++ in place. Their published figures track the result: memory-safety bugs were 76% of Android’s vulnerabilities in 2019, 35% in 2022, 24% in 2024 and, in 2025, under 20% for the first time. The explanation they give is the age of code — most memory bugs live in new code, so stopping them at the source shrinks the share every year even while the old code stays. In November 2025 they added the density: about 0.2 memory-safety vulnerabilities per million lines of Rust, against roughly 1,000 per million in their C and C++. And two delivery numbers nobody expected: Rust changes were rolled back about four times less often, and spent about 25% less time in review.
The fifth case — the unwrap() that panicked Cloudflare’s proxy fleet in November 2025 — is told under errors and panics. Read together, the five draw the same line as the table of what the compiler cannot check: Rust removes a class of bugs completely, and leaves every other class exactly where it was.
Where it degrades
Rust’s guarantee is precise and its costs are paid up front. A team pays in learning time and compile time before the first release, and is repaid in a class of bug that never ships. It degrades when the guarantee is read as wider than it is, or when the cost is paid for a problem that did not need it:
unwrap()andexpect()on inputs that can actually fail, turning one bad file into a fleet-wide panic..clone()everywhere to quiet the borrow checker, until the hot path spends its time copying (row 1).Rc<RefCell<…>>webs that move every borrow error to run time and leak on cycles.unsafeblocks with no// SAFETY:comment, no Miri run, and a safe API that trusts them.- Blocking calls inside async tasks, stalling every task on the same runtime thread.
- The belief that a Rust service cannot have security bugs — logic, escaping, authorisation and configuration are all still yours.
- Integer overflow in release builds, wrapping silently where a debug build would have panicked.
- A dependency tree of hundreds of crates, never checked against RustSec.
- Compile times nobody budgeted for, slowing every engineer’s feedback loop.
- A rewrite of working code chosen for the language rather than for a measured problem.
The honest limit is fit. Rust is the best tool available when memory safety and predictable latency matter together — a proxy, a database engine, a browser component, a kernel driver, firmware, or a hot path that a garbage collector keeps interrupting. For a CRUD service whose time goes on waiting for a database, its guarantees protect against bugs that Go, Java or Python already prevent with a collector, and the team pays in speed of change for safety it already had. Android’s lesson cuts the same way: the gain came from writing new low-level code in Rust, not from rewriting everything in it.
The practical check takes ten minutes. Run rustc --version where you build, and read the edition in Cargo.toml. Search the request path for unwrap( and expect(, and ask what input breaks each one. Count the unsafe blocks and check that each has a SAFETY comment. Make sure CI runs Clippy and cargo audit. And look for blocking calls — std::fs, std::thread::sleep, synchronous clients — inside async fn.
When to use it
Use it when
- Infrastructure where memory safety and predictable latency matter together — proxies, databases, runtimes.
- Code that handles untrusted input at a low level — parsers, codecs, network protocols.
- Services with a huge, long-lived heap, where a collector’s pauses are the problem.
- Fast cores of libraries called from Python, Node or C, where Rust replaces C.
Avoid it when
- CRUD services that mostly wait on a database — a collector already gives you the safety.
- Teams on a tight deadline with no Rust experience; the learning curve lands in the first months.
- Prototypes whose shape changes weekly; the compiler makes every restructuring explicit.
- Rewrites of stable, working code without a measured problem that the rewrite fixes.
Sources and method
What this page stands on
- 17 sources
- 2 documented failures
- Checked October 13, 2026
Practices and where the terms come from
the twelve compiler errors, ownership, concurrency and unsafe- GuideThe Rust Programming Language — Understanding Ownership (opens in a new tab)doc.rust-lang.org
- GuideThe Rust Programming Language — Fearless Concurrency (opens in a new tab)doc.rust-lang.org
- GuideThe Rust Programming Language — Unsafe Rust (opens in a new tab)doc.rust-lang.org
- StandardThe Rust Reference (opens in a new tab)doc.rust-lang.org
- DocumentationRust error codes index (opens in a new tab)doc.rust-lang.org
- GuideThe Rust Edition Guide (opens in a new tab)doc.rust-lang.org
- GuideThe Cargo Book (opens in a new tab)doc.rust-lang.org
- Documentationtokio::task::spawn_blocking (opens in a new tab)docs.rs
- ArticleAnnouncing Rust 1.0 (opens in a new tab)Rust Blog · May 15, 2015
The failures this page learns from
BatBadBut and Cloudflare’s November 2025 outage, in “Five cases”Research
Discord, Pingora, Android and Linux: what the switch measured- ArticleJesse Howarth — Why Discord is switching from Go to Rust (opens in a new tab)Discord · Feb 4, 2020
- ArticleYuchen Wu, Andrew Hauck — How we built Pingora, the proxy that connects Cloudflare to the Internet (opens in a new tab)Cloudflare · Sep 14, 2022
- ResearchMemory Safe Languages in Android 13 (opens in a new tab)Google Online Security Blog · Dec 1, 2022
- ResearchEliminating Memory Safety Vulnerabilities at the Source (opens in a new tab)Google Online Security Blog · Sep 25, 2024
- ResearchJeff Vander Stoep — Rust in Android: move fast and fix things (opens in a new tab)Google · Nov 13, 2025
- ArticleThe (successful) end of the kernel Rust experiment (opens in a new tab)LWN.net · Dec 2025
Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.
Found this useful?
Share it with someone who is working on the same problem.