August 1, 20255 min

Stack vs. Heap in Rust: Where Does Your Data Live?

m
mayo

Rust uses stack and heap allocation to manage memory, with distinct characteristics for each. Understanding their differences and how Rust decides where to allocate data is key to writing efficient and safe code.

Stack (LIFO, fixed size) Heap (dynamic size) let x = 5; (i32) let s = String::from(..) boxed: ptr ● "heap" (String bytes) 42 Box::new(42)

Stack vs. Heap in Rust

Stack Heap
Fast allocation/deallocation (LIFO). Slower allocation (dynamic).
Fixed, known size at compile time. Size can grow (e.g., String, Vec).
Automatic cleanup (no free() needed). Manual management (via Drop trait).
Used for primitive types (i32, bool), small structs. Used for large, dynamic data (String, Box<T>).

How Rust Decides Where to Allocate

By Default → Stack

If a type has a fixed size (e.g., i32, arrays, structs with no String/Vec), it is allocated on the stack.

Example:

let x = 5; // Stack (i32 is fixed-size)

Explicit Heap Allocation

Use types like Box<T>, String, Vec, etc., to allocate on the heap.

Example:

let s = String::from("heap"); // Heap (growable UTF-8 string)
let boxed = Box::new(42);     // Heap (Box<T>)

Move Semantics

When a value is moved, its heap data is transferred, not copied, ensuring efficient memory management.

Example:

let s1 = String::from("hello"); // Heap-allocated
let s2 = s1; // Moves ownership (heap data not copied)
// println!("{}", s1); // ERROR: s1 is invalidated
Before After let s1 = String::from("hello"); let s2 = s1; stack s1 : ptr | len 5 | cap 5 3-word header s1 — moved out, cannot be used s2 : ptr | len 5 | cap 5 same ptr value, bitwise copy move heap "hello" "hello" untouched — zero bytes copied One owner per buffer: s1 must be invalidated so the heap block is freed exactly once.

Key Takeaways

Stack: Fast, fixed-size, automatic.
Heap: Flexible, dynamic, manual (via smart pointers).
✅ Rust defaults to stack but uses heap for growable/unknown-size data.

Follow-Up: When would you force heap allocation?

  • For large structs (avoid stack overflow).
  • When you need dynamic dispatch (e.g., Box<dyn Trait>).
  • To share ownership across threads (Arc<T>).