Rust has a reputation problem. Not with performance or correctness, those are well-established. The problem is with time: specifically, how long it takes to write Rust compared to Python, Go, or JavaScript for ordinary application code. Most of that overhead comes from a handful of friction points that new Rust developers hit repeatedly: lifetime annotations, borrow checker rejections, error type proliferation, and the perpetual question of when to use &str versus String. A recent post on hamy.xyz frames the situation well — you can capture most of Rust’s value (memory safety, fearless concurrency, predictable performance) while deliberately avoiding the parts that require deep systems programming expertise. That framing is correct, and it is worth being precise about which patterns actually do the work and why the tradeoffs are sound.
Own Your Types, Borrow Your Parameters
The single most impactful change for reducing Rust friction in application code is adopting a clear rule about owned versus borrowed types: use owned types in struct fields and return types, and use borrows in function parameters where you only need to read.
// Lifetime annotations everywhere -- difficult to maintain
struct Config<'a> {
name: &'a str,
base_url: &'a str,
api_key: &'a str,
}
// Owned types -- no lifetime juggling
struct Config {
name: String,
base_url: String,
api_key: String,
}
The cost is a few heap allocations at startup. The benefit is that Config can be stored in a HashMap, passed across threads, returned from an async function, and wrapped in Arc<T> without touching a single lifetime annotation. For configuration data, connection strings, and other long-lived values, this is the right tradeoff.
At call sites, you still get efficiency by accepting &str in functions that only read:
fn validate_url(url: &str) -> bool {
url.starts_with("https://")
}
// Both work -- Rust's deref coercions handle the conversion
validate_url(&config.base_url);
validate_url("https://example.com");
This hybrid — owned storage, borrowed reads — is idiomatic Rust. The same principle applies across the standard library’s paired types: String / &str, Vec<T> / &[T], PathBuf / &Path. None of this is a workaround; it is how the standard library itself is designed.
Clone First, Profile Later
The borrow checker will, at some point, reject code that you believe should be valid. The instinct is to fix it by reasoning through ownership graphs and adding lifetime parameters. The better instinct, for most application code, is to call .clone().
// Fighting the borrow checker with lifetime parameters
fn process_items<'a>(items: &'a [Item], filter: &'a str) -> Vec<&'a Item> {
items.iter().filter(|i| i.label.contains(filter)).collect()
}
// Cloning and moving on
fn process_items(items: Vec<Item>, filter: String) -> Vec<Item> {
items.into_iter().filter(|i| i.label.contains(&filter)).collect()
}
Cloning a String costs a heap allocation and a memcpy. On modern hardware, cloning a few hundred bytes of string data takes tens of nanoseconds. The Rust Performance Book makes this point plainly: allocation is rarely the bottleneck in real applications. Cache misses and branch mispredictions cause far more slowdown than a strategically placed .clone(). Profile before optimizing, and use cargo flamegraph or criterion to measure actual hotspots rather than intuited ones. You will often find that the clone you spent twenty minutes avoiding was called once per request on a 40-byte string.
The right workflow is: write the obvious thing, get it working, measure, then tighten the specific paths that show up in the profile. Not the reverse.
Error Handling Without the Ceremony
Rust’s Result<T, E> type is excellent. The ecosystem of error handling libraries built on top of it is also excellent. The ceremony of defining a custom error enum for every module, manually implementing Display and From conversions, is not excellent, and you largely do not need it.
For application code — binaries, CLI tools, server logic — the anyhow crate by David Tolnay provides a single error type that accepts any error, propagates cleanly with ?, and prints a readable cause chain:
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config at {path}"))?;
serde_json::from_str(&content)
.context("config was not valid JSON")
}
The output when this fails looks like:
Error: failed to read config at /etc/myapp/config.toml
Caused by:
No such file or directory (os error 2)
For library crates where callers need to match on specific error variants, thiserror provides a derive macro that generates all the boilerplate from an annotated enum:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("connection failed: {0}")]
Connection(#[from] sqlx::Error),
#[error("record not found: id={0}")]
NotFound(i64),
}
The #[from] attribute generates a From<sqlx::Error> implementation automatically, so ? works across the boundary. thiserror compiles to nothing at runtime — zero overhead. The community standard is: thiserror in libraries, anyhow in applications. They compose well: a library returns typed errors, an application wraps them with anyhow context.
Arc Is the Right Answer to Shared State
When multiple parts of a program need to share access to data and lifetime annotations become unwieldy, Arc<T> is often the correct tool. Cloning an Arc increments an atomic counter — it is O(1) and does not copy the underlying data.
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
config: Arc<Config>,
db: Arc<Pool<Postgres>>,
}
// Spawning tasks is trivial -- Arc::clone is cheap
let state = AppState { config: Arc::new(config), db: Arc::new(pool) };
tokio::spawn(handle_request(state.clone()));
tokio::spawn(run_background_jobs(state.clone()));
For shared mutable state, Arc<Mutex<T>> or Arc<RwLock<T>> cover the common cases. This pattern eliminates a wide class of lifetime problems and maps cleanly onto how async runtimes like tokio expect state to be organized. The tradeoff is a small amount of atomic overhead on clone and a potential contention point on the lock; both are measurable and often irrelevant compared to I/O wait times.
The Performance Reality Check
A reasonable concern about high-level Rust is that all these clones and heap allocations eat into the performance advantage over Go or Python. The data does not support that concern for typical application workloads.
The TechEmpower Framework Benchmarks consistently show Rust frameworks like Axum and Actix-web in the top tier — well ahead of Go’s Gin and Fiber — for JSON serialization, database query throughput, and plaintext response benchmarks. The Benchmarks Game shows Rust roughly equivalent to C/C++ and consistently 2-5x faster than Go across CPU-bound tasks. Python is typically 30-100x slower.
High-level cloning Rust introduces perhaps 10-30% overhead compared to carefully zero-copy Rust in allocation-heavy paths. That still puts it comfortably ahead of Go in most benchmarks. The key advantage is not the absence of clones; it is the absence of a garbage collector. Deterministic memory reclamation means no GC pauses, no stop-the-world latency spikes, and predictable p99 and p999 response times. You can clone liberally and still get the latency profile that GC languages cannot reliably deliver.
How Rust Got Here
It is worth noting that the complexity people complain about in Rust today is significantly less than what existed in 2018. Non-Lexical Lifetimes (NLL), which stabilized in the 2018 Edition, made borrows end at their last actual use rather than at the closing brace of their lexical scope. This eliminated an entire category of spurious borrow checker errors that plagued early Rust. Stabilization of async/await in Rust 1.39 replaced a tangle of combinator chains with code that reads like synchronous logic. Stabilization of async fn in traits in Rust 1.75 removed the need for the async-trait proc-macro in most cases.
The upcoming Polonius borrow checker, already available on nightly, accepts more valid programs that the current implementation rejects — including the long-standing issue with returning references from conditional branches that read from a map. The trajectory is toward a compiler that gets out of your way more often, not less.
When You Do Need the Hard Parts
High-level Rust is not appropriate for every Rust use case. If you are writing a memory allocator, implementing a lock-free data structure, building an operating system kernel, or working with raw hardware registers, the full ownership model and unsafe exist for good reasons. The Rust embedded ecosystem (embedded-hal, svd2rust) leans heavily on zero-cost abstractions and type-level enforcement of hardware constraints.
For web services, CLI tools, Discord bots, data pipelines, and most of the software developers actually ship, the high-level patterns are sufficient and the performance is more than adequate. The borrow checker still catches use-after-free bugs, data races, and the whole class of memory safety errors that C and C++ leave to the programmer. You get that guarantee without needing to earn it by mastering every corner of the type system.
The point is not that lifetimes are unimportant. The point is that you can write a substantial amount of correct, fast, production Rust without being an expert in them. Start with owned types, clone where it’s convenient, reach for anyhow and Arc, and add complexity only where profiling and requirements justify it.