Functions

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