Stack Overflow and Recursion Limits
Every thread in a running program has a fixed-size region of memory called the call stack. A stack overflow occurs at runtime when active stack frames grow beyond the end of the thread's allocated stack—most commonly due to unbounded or excessively deep recursion, or allocating multi-megabyte arrays directly on the stack. Rust also enforces a separate compile-time recursion limit (#![recursion_limit]) for macros, const evaluation, and trait resolution.
Runtime Call Stack Limits and Guard Pages
When the operating system launches a program, it allocates a fixed virtual memory region for the main thread's call stack and places an unmapped guard page immediately below the bottom of the stack:
| Thread Type | Default Stack Size | How to Customize |
|---|---|---|
Main thread (fn main()) |
OS-dependent: typically 8 MB on macOS and Linux, 1 MB on Windows | OS shell ulimit -s (Unix) or linker flag |
Spawned threads (std::thread::spawn) |
2 MB (2 * 1024 * 1024 bytes) across all platforms (or $RUST_MIN_STACK) |
std::thread::Builder::new().stack_size(bytes) |
Thread Stack Memory Layout:
High Address +-----------------------------------+
| main() stack frame |
|-----------------------------------|
| recurse(1) stack frame |
|-----------------------------------|
| recurse(2) stack frame |
| ... | (Stack grows downward)
| recurse(26_000) stack frame |
Stack Bottom +-----------------------------------+ <-- Stack limit reached!
|//////// OS Guard Page ////////////| <-- Touching this page triggers SIGSEGV /
+-----------------------------------+ STATUS_STACK_OVERFLOW!
When a function pushes a frame that touches the OS guard page, the CPU raises a hardware page fault. The Rust runtime catches the guard-page fault, prints a deterministic message to standard error, and aborts the process immediately (a stack overflow cannot be caught with std::panic::catch_unwind):
#[allow(unconditional_recursion)]
fn infinite_recurse(depth: u64) -> u64 {
// Prevent LLVM from optimizing away the tail call by doing work after the call:
1 + infinite_recurse(depth + 1)
}
fn main() {
println!("Starting deep recursion...");
// Calling `infinite_recurse(0)` exhausts the stack and aborts!
let _ = infinite_recurse;
}
If infinite_recurse(0) is called at runtime, the process terminates with:
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Abort trap: 6
Causes of Runtime Stack Overflow and How to Fix Them
1. Missing Base Case or Unbounded Recursion Depth
If a recursive function's depth scales linearly with untrusted or large input ($O(N)$ depth where $N$ can be $100,000+$), a 2 MB or 8 MB stack will overflow.
Fix: Rewrite linear recursion as an iterative while or for loop (which uses $O(1)$ stack space), or use an explicit heap-allocated stack (Vec<State>):
// Iterative conversion uses O(1) stack space even for N = 10_000_000:
fn sum_iterative(n: u64) -> u64 {
let mut acc = 0;
for i in 1..=n {
acc += i;
}
acc
}
// Explicit heap stack (`Vec`) for deep tree/graph traversal without call-stack risk:
fn sum_with_heap_stack(n: u64) -> u64 {
let mut work_stack: Vec<u64> = vec![n];
let mut total = 0;
while let Some(current) = work_stack.pop() {
if current > 0 {
total += current;
work_stack.push(current - 1);
}
}
total
}
fn main() {
println!("Iterative: {}", sum_iterative(1_000));
println!("Heap stack: {}", sum_with_heap_stack(1_000));
}
Iterative: 500500
Heap stack: 500500
2. Giant Stack-Allocated Arrays ([u8; 10_000_000])
Declaring a multi-megabyte fixed-size array as a local variable (let huge = [0_u8; 10_000_000];) tries to allocate 10 MB directly inside the current function's stack frame, immediately overflowing an 8 MB stack on entry!
Fix: Allocate large buffers on the heap using vec![0_u8; 10_000_000] (which puts only a 24-byte Vec header on the stack and stores the 10 MB buffer on the heap):
fn main() {
// Safe: only 24 bytes on the stack; 10 MB lives on the heap!
let heap_buffer: Vec<u8> = vec![0_u8; 10_000_000];
println!("Allocated {} MB safely on the heap.", heap_buffer.len() / 1_000_000);
}
Allocated 10 MB safely on the heap.
3. Spawning a Thread with a Larger Stack Size
If an algorithm genuinely requires deep recursion on a known dataset, you can spawn a dedicated thread with a custom stack size using std::thread::Builder::new().stack_size(...):
use std::thread;
fn deep_recursion(n: u64) -> u64 {
if n == 0 {
0
} else {
1 + deep_recursion(n.saturating_sub(1))
}
}
fn main() {
// Spawn a worker thread with a 32 MB stack:
let builder = thread::Builder::new()
.name(String::from("deep-worker"))
.stack_size(32 * 1024 * 1024);
let handle = builder.spawn(|| deep_recursion(50_000)).unwrap();
let result = handle.join().unwrap();
println!("Completed 50,000 recursive frames on 32 MB stack: result = {result}");
}
Completed 50,000 recursive frames on 32 MB stack: result = 50000
Compile-Time Recursion Limits (#![recursion_limit])
Separately from runtime call stacks, rustc enforces a compile-time recursion limit (defaulting to 128) when expanding recursive macros (macro_rules!), resolving deeply nested trait bounds, or evaluating const fn calls at compile time. This prevents a buggy macro or trait implementation from freezing the compiler in an infinite loop.
If a legitimate macro or compile-time evaluation exceeds 128 nested steps, rustc reports an error and tells you how to raise the limit with #![recursion_limit = "256"] at the top of your crate root:
#![recursion_limit = "256"]
const fn const_countdown(n: u32) -> u32 {
if n == 0 { 0 } else { const_countdown(n - 1) }
}
const DONE: u32 = const_countdown(200);
fn main() {
println!("Compile-time recursion completed: DONE = {DONE}");
}
Compile-time recursion completed: DONE = 0