Why Rust Closures Are Fast

Closures show up everywhere in hot paths — iterators, callbacks, thread spawns. If you're writing performance-sensitive code, you probably use them without thinking twice. But why do they cost so little in Rust compared to JavaScript, Python, or Java?

It comes down to three things: allocation, dispatch, and cleanup.

Allocation

In an interpreted language, a closure usually gets allocated on the heap by default. The runtime doesn't know how long that closure will live, so it plays safe and puts it somewhere it can track.

function makeCounter() {
  let count = 0;
  return () => ++count; // heap allocation, tracked by the GC
}

Rust doesn't do this. By default, a closure lives on the stack. It only ends up on the heap if you explicitly ask for it — wrapping it in a Box, storing it in a Vec, or passing it as a trait object.

let mut count = 0;
let mut counter = || { count += 1; count }; // stack, no allocation

You pay for the heap only when you actually need it. That's the first win: no implicit cost.

Dispatch

Here's the bigger one. In JavaScript, Python, or Java, the runtime doesn't know the concrete type of a closure ahead of time. Every call goes through dynamic dispatch — the runtime figures out at call time what it's actually invoking.

Rust knows the type of every closure at compile time. Each closure gets its own anonymous type, and the compiler monomorphizes the code around it. That means the call isn't just fast — it can be inlined entirely, because the compiler knows exactly what's being called and where.

fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(x) // often inlined, no indirection
}

No runtime lookup. No guessing. The compiler already did the work before your program even ran.

Cleanup

This is the one people underestimate. In garbage-collected languages, memory doesn't disappear the moment you're done with it — it disappears when the GC gets around to scanning for it. That scan costs time, and it happens while your program is trying to do other things.

Rust closures follow the same ownership rules as everything else. When a closure goes out of scope, Drop runs immediately. Deterministic, no scanning, no pause. The moment it's not needed, it's gone — and your program never sits idle waiting for a collector to catch up.

So what

You get predictability and speed, but you have to think about ownership up front instead of trusting a runtime to sort it out later.

That's the pattern with most of Rust's performance story, honestly. It's not that Rust is "smarter." It's that Rust asks you to answer questions earlier — where does this live, how long does it live — so the runtime never has to guess.