Rust Is Not Always Faster and the Cases That Prove It
Rust gives you control over speed, not speed for free. Debug builds, extra clones, blocking calls and wrong algorithms can all make Rust code slower than a tuned alternative.

"Rewrite it in Rust and it will be faster" is true often enough to be dangerous. It is false in ways that waste weeks: a debug build that runs slower than the Python it replaced, a rewrite that matches a C library you were already calling, an optimizer that deletes the loop you were trying to time. This post measures four of those cases on one machine so you can see how large the effects are. Every number below was produced by the programs shown, built with Rust 1.99 on an Apple silicon laptop, and the Python comparison used Python 3.14 without extra libraries.
The point is not that Rust is slow. It is that the language is one factor among several, and the others, the build mode, the data structure, which library actually does the work, usually dominate. Knowing which factor you are measuring is the skill.
Case one: the build mode changes the answer by a multiple
cargo run and cargo build compile in debug mode: no optimization, overflow checks, debug assertions. Programs in that mode are slow, and the slowness is not subtle. This program counts word frequencies two ways, with owned keys and with borrowed ones, and runs a tight arithmetic loop:
use std::collections::HashMap;
use std::hint::black_box;
use std::time::Instant;
fn word_counts_cloning(text: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word.to_string()).or_insert(0) += 1;
}
counts
}
fn word_counts_borrowing(text: &str) -> HashMap<&str, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
counts
}
/// Each iteration depends on the last, so the compiler cannot replace the loop with a formula.
fn xorshift_sum(n: u64) -> u64 {
let mut x = 88172645463325252u64;
let mut acc = 0u64;
for _ in 0..n {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
acc = acc.wrapping_add(x);
}
acc
}
fn main() {
let text = "the quick brown fox jumps over the lazy dog ".repeat(200_000);
let t = Instant::now();
let a = word_counts_cloning(black_box(&text));
println!("owned keys: {:>8.1?} ({} distinct)", t.elapsed(), a.len());
let t = Instant::now();
let b = word_counts_borrowing(black_box(&text));
println!("borrowed keys: {:>8.1?} ({} distinct)", t.elapsed(), b.len());
let t = Instant::now();
let s = xorshift_sum(black_box(200_000_000));
println!("xorshift loop: {:>8.1?} (result {s})", t.elapsed());
}The loop is an xorshift random number generator accumulated 200 million times, a dependency chain that the compiler cannot collapse. Built with plain rustc (debug) and with rustc -O (release), and run three times each, the timings were steady to within a few percent:
build owned keys borrowed keys xorshift loop
debug 629 ms 534 ms 1.5 s
release 88 ms 34 ms 0.40 s(Numbers are the fastest of three runs.) Same source, same result, and the loop is 3.8 times slower in debug. For code with iterator chains and generic collections the gap is larger, because abstractions that cost nothing in release mode are real function calls in debug. The word-count functions show it: the owned-key version took 629 ms in debug and 88 ms in release, a factor of 7.2, and the borrowed-key version took 534 ms against 34 ms, a factor of 16. A developer who benchmarks cargo run and concludes that Rust is slow has measured the debugger's build. Use cargo run --release or cargo bench for any comparison, and read Cargo release profile settings for what else the profile controls.
Case two: the optimizer can delete your benchmark
The first draft of this experiment summed squares over 200 million numbers, with a function like this:
fn sum_squares(n: u64) -> u64 {
(0..n).map(|i| i.wrapping_mul(i)).fold(0u64, |a, b| a.wrapping_add(b))
}In release mode it reported 0.0ns and the right answer. The compiler had recognized that the sum of squares has a closed form and replaced the loop with a formula, so it ran 200 million iterations' worth of work in no time at all. Wrapping the input in std::hint::black_box did not stop it:
sum of squares with black_box: 0.0ns (result 5323371213918391040)That is an honest limitation of micro-benchmarks. When the result is correct and the time is zero, the compiler found a shortcut, which tells you about LLVM and nothing about the cost of the algorithm you thought you were timing. The xorshift loop above was chosen because each iteration depends on the last, so no formula exists and the work is real. To benchmark small functions, use a harness like criterion, and make the inputs vary and the outputs observable. A speedup that looks too good to be true is usually this.
Case three: the algorithm and the data structure beat the language
Compare the two word-count functions in release mode, built on the same text of 1.8 million words and eight distinct ones:
fn word_counts_cloning(text: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word.to_string()).or_insert(0) += 1;
}
counts
}
fn word_counts_borrowing(text: &str) -> HashMap<&str, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
counts
}owned keys: 88.0ms (8 distinct)
borrowed keys: 34.5ms (8 distinct)Both are Rust, both are release builds, and one is 2.5 times faster. The first allocates a new String for every word, 1.8 million allocations, to use as a lookup key, even though only eight keys ever get stored. The second borrows slices of the original text and allocates nothing per word. Writing word.to_string() out of habit, or .clone() to make the borrow checker quiet, is the commonest way for correct Rust to lose its advantage. This is why profile-first advice exists: the allocator, not the language, was the cost.
Case four: the competitor was already compiled code
Now compare against Python on the same text, the way a rewrite decision gets made. Pure Python counting with a dictionary:
import collections, time
text = "the quick brown fox jumps over the lazy dog " * 200_000
t = time.perf_counter()
c = collections.Counter(text.split())
print(f"Counter(text.split()): {(time.perf_counter() - t) * 1000:.1f} ms")
t = time.perf_counter()
d = {}
for w in text.split():
d[w] = d.get(w, 0) + 1
print(f"python loop with dict: {(time.perf_counter() - t) * 1000:.1f} ms")Counter(text.split()): 136.8 ms
python loop with dict: 253.3 msThe release Rust versions took 88 ms (owned keys) and 34 ms (borrowed). So Rust is 1.6 times faster than Python's Counter in the careless version and 4 times faster in the careful one, not 100 times. Counter is implemented in C, and str.split is too, so most of the work already runs at native speed; the Python interpreter is only involved in a handful of calls. That is typical of Python code that leans on library functions, and it is why a rewrite of such code, for speed alone, often disappoints.
Contrast the arithmetic loop. The same xorshift written as a Python for loop took 8.5 seconds for 20 million iterations, which extrapolates to about 85 seconds for 200 million. The Rust release build did the 200 million in 0.40 seconds. That is a factor of roughly 215, and it is the case where the interpreter executes every operation, which is exactly where compiled code wins by two orders of magnitude.
| Workload | Python | Rust release | Rust advantage |
|---|---|---|---|
Word count with Counter (C inside) | 137 ms | 34 to 88 ms | 1.6 to 4 times |
| Word count with a Python loop | 253 ms | 34 to 88 ms | 3 to 7 times |
| Tight arithmetic loop, 200M iterations | about 85 s | 0.40 s | about 215 times |
Read the table as a map of where the language matters. It matters a great deal when the program's own loop is the cost, and little when the program is glue around fast libraries.
Where Rust wins that these numbers do not show
Raw throughput on a single task is the weakest of Rust's advantages. The stronger ones appear elsewhere. Tail latency: with no garbage collector, a Rust service has no pause that lands on a random request, so the 99th percentile stays close to the median. Memory: a Rust program holds what it allocates, with no runtime heap overhead, so a service that needed 2 GB under a managed runtime may need a fraction of that. Predictability: the same code behaves the same under load, since there is no JIT warm-up and no background collector competing for cores. And parallelism without data races: the compiler lets you spread work across threads without the usual fear. None of these appears in a single-threaded benchmark on one core.
A checklist before blaming or crediting the language
- Is the build
--release? If not, stop. - Does the timed code do real work, or did the optimizer fold it away? Vary the input, use the result.
- Is the Rust code allocating where it need not? A
to_string(),clone()orcollect()inside a loop is the first thing to check. - What is the baseline actually running? If it calls NumPy, a database or a C library, the comparison is to compiled code.
- Does the speed matter to anyone? A batch job that runs for 40 ms is not a performance problem in either language.
If, after that, the program is CPU-bound, spends its time in its own loops and the numbers justify it, Rust is an excellent tool for the rewrite. The method for finding out what the time is spent on is in profiling Rust programs from flamegraph to a fix.


