DevelopmentAll levels

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.

#ErrorThe rule it enforcesThe usual fix
1E0382 · use of a moved valueAssigning a String, a Vec or a Box moves it; the old name is deadBorrow it with &, .clone() it, or restructure so one owner is enough
2E0384 · cannot assign twice to an immutable variableVariables are immutable unless declared mutlet mut, or shadow the name with a new let
3E0596 · cannot borrow as mutableChanging a value through a reference needs a mut binding and a &mutDeclare it mut; take &mut self in the method
4E0499 · cannot borrow as mutable more than onceAt most one &mut to a value at a timeFinish with one before taking the next; borrow separate fields of a struct
5E0502 · cannot borrow as mutable because it is also borrowed as immutableReaders or one writer — never both at onceCopy out what you read, then write; or let the read borrow end first
6E0505 · cannot move out because it is borrowedA value cannot leave while someone holds a reference to itLet the borrow end first, or clone
7E0507 · cannot move out of a value behind a referenceA borrower may look, not take.clone(), std::mem::take, or Option::take()
8E0515 · cannot return a reference to a local variableA reference may not outlive what it points toReturn the owned value — String, not &str
9E0597 · borrowed value does not live long enoughThe value is dropped while a reference to it is still in useMove the value to an outer scope, or own it
10E0106 · missing lifetime specifierA returned reference must say which input it borrows fromName the lifetime — fn f<'a>(x: &'a str) -> &'a str — or return an owned value
11E0373 · closure may outlive the current functionA thread may run longer than the function that started itmove the data into the closure; std::thread::scope for borrowed data
12E0277 · Rc<…> cannot be sent between threads safelyOnly Send types may move to another thread, and only Sync types may be shared; Rc is neitherArc 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 — in Java, Go, Python — answers at run time by finding values nothing points to any more, and pays in 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 String is three words on the stack — a pointer to its bytes on the heap, a length and a capacity. Moving it copies those three words to the new owner and marks the old name unusable; the bytes on the heap never move. Because there is only ever one owner, the compiler knows exactly where the value stops being used and frees it there, once — no garbage collector and no manual free. Using the old name after a move is error E0382, row 1 of the table above.

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 shared borrow and a mutable borrow of the same value may not be alive at the same time. What counts is where each borrow is last used, not where the block ends: on the left the shared borrow first is finished by line 3, so the push on line 4 is allowed. On the right the same lines in a different order keep first alive across the push, and push may reallocate the vector — first would then point at freed memory. The compiler refuses with E0502, row 5 of the table above.

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.

BugIn safe RustWhy
Use after free, double free, dangling pointerImpossibleOwnership and borrowing (rows 1–10)
Data race — two threads writing one variableImpossibleSend and Sync (rows 11–12)
Reading past the end of an arrayStopped at run time with a panicEvery index is bounds-checked
Integer overflowA panic in debug builds; silent wrap-around in release buildsChecks are off in release unless overflow-checks = true
Memory leakPossible, and considered safeAn Rc cycle, mem::forget, Box::leak
DeadlockPossibleTwo threads taking two Mutexes in opposite order
Race condition in logic — check, then actPossibleEach step is synchronised; the pair is not
A panic — unwrap(), expect(), an indexPossible; it stops the threadA panic is safe; it is not handled
Logic errorsPossibleThe 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 , so each promise is checked once, by the people who wrote it, rather than at every call.

Most Rust code never writes unsafe: it calls safe APIs, and the standard library writes the unsafe for it. Inside Vec a few unsafe blocks write through raw pointers, and each rests on an invariant the compiler cannot see — here, that the length is below the capacity once space has been reserved. The compiler still checks everything else, including borrows inside the block; what it takes on trust is that one invariant. That is why one wrong unsafe block can break the guarantee for every safe caller above it, and why such blocks are kept small, commented and tested under Miri.

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 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.

TypeSendSyncUse it for
i32, String, Vec<T>YesYesPlain data
Rc<T>NoNoShared ownership on one thread — its count is not atomic
Arc<T>YesYesShared ownership across threads; the count is atomic
RefCell<T>YesNoChange through & on one thread, checked at run time
Mutex<T>YesYesChange through & across threads, one at a time
std::sync::MutexGuardNoYesHolding the lock — it must be released on the thread that took it

The result is the property the Rust community calls fearless concurrency: a 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.

EditionCompilerWhat changed that you will meet
20151.0, May 2015The original language
20181.31, Dec 2018async and dyn become keywords; a simpler module system; non-lexical lifetimes
20211.56, Oct 2021Closures capture individual fields; TryFrom and TryInto in the prelude; arrays iterate by value
20241.85, Feb 2025unsafe 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 against the RustSec advisory database in . 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.

CaseWhat happenedWhat it shows
Discord, Feb 2020Read States moved from Go to Rust; the two-minute GC spikes disappearedMemory freed at a known point — no collector to pause
Cloudflare Pingora, Sep 2022A replacement for Nginx in Rust: about 70% less CPU and 67% less memoryThe gain came from the architecture as much as from the language
CVE-2024-24576, Apr 2024The standard library escaped arguments to Windows batch files wronglySafe Rust does not prevent logic bugs, even in std
Android, 2019–2025Memory-safety bugs fell from 76% of vulnerabilities to under 20% as new code moved to RustNew code matters more than rewriting old code
Cloudflare, 18 Nov 2025An unwrap() on an oversized configuration file panicked the proxy fleetA 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 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.

Google's Android team began writing new low-level code in memory-safe languages, Rust among them, around 2019, and left the existing C and C++ in place. The share of Android vulnerabilities caused by memory-safety bugs fell from 76% in 2019 to 35% in 2022 and 24% in 2024, and in 2025 it was below 20% for the first time — far below the roughly 70% that Microsoft and Chromium have reported for their code. Google's explanation is the age of code: most memory bugs are in new code, so changing the language of new code shrinks the share every year. Years without a published figure are not drawn.

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() and expect() 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.
  • unsafe blocks 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

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.