C++ Stack Overflow: Recursion, Large Locals, and How to Fix

Key takeaways

A segfault in code without any pointers is often the stack running out. The post shows how to tell infinite recursion from legitimately deep recursion, why a large local array can crash on entry to a function, how to rewrite tree traversal with an explicit std::stack, and where tail-call patterns help once optimizations are enabled.

Stack exhaustion often shows up as a segfault; the segmentation fault guide covers the other causes.

Introduction: “My recursive function crashes”

Stack overflow happens when stack memory is exhausted. Common causes: infinite recursion, very large locals, and very deep recursion.

void foo() {
    foo();
}
int main() {
    foo();
}
// Linux/macOS: SIGSEGV; Windows: stack overflow

The reason this looks like a pointer bug is how the limit is enforced. The operating system reserves a region of address space for the stack and places a guard page below it that is not mapped. As the stack grows downward, the first access into the guard page faults, and the kernel delivers SIGSEGV (Linux, macOS) or raises EXCEPTION_STACK_OVERFLOW (Windows, code 0xC00000FD). There is no C++ exception to catch; the process dies. Because even the signal handler needs stack space, a plain handler cannot run either, unless it was installed on an alternate stack with sigaltstack.

Note that this tiny example may not crash at all with optimizations on: foo() calls itself in tail position, so at -O2 GCC and Clang can turn it into an infinite loop, and the program simply hangs. Infinite recursion with no side effects is undefined behavior, so either outcome is allowed. That is a useful reminder that stack behavior differs between debug and release builds, in both directions. This article covers:

  • Four major causes
  • Recursion depth limits
  • Adjusting stack size (last resort)
  • Moving work to the heap
  • Tail recursion and iterative forms

What is stack overflow?

Stack memory

Each function call creates a stack frame for locals and return addresses. Typical default stack limits (order of magnitude):

  • Linux: ~8 MB for the main thread (ulimit -s), secondary threads usually 8 MB with glibc
  • Windows: ~1 MB (set in the executable header, applies to every thread by default)
  • macOS: ~8 MB for the main thread, 512 KB for secondary threads When recursion or locals exceed the limit, the program crashes.

A frame holds the return address, saved registers, and every local variable of the function, including arrays and objects stored by value. Its size is fixed at compile time for most functions, so a function with a 4 MB local array uses 4 MB of stack on every call, even if the array is used only on one branch. Debug builds use noticeably more stack than release builds, because the optimizer normally keeps many locals in registers and reuses slots, while -O0 gives each variable its own slot and sanitizers add redzones around them. A recursion that works in release can therefore overflow in a debug or ASan build, which is a common source of “only crashes in tests” reports.


Four major causes

Infinite recursion

int factorial(int n) {
    return n * factorial(n - 1); // missing base case
}

Fix: Base case.

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

The base case must also be reachable for every input. if (n == 0) return 1; looks correct but recurses forever for a negative n, which is why n <= 1 is the safer test. The same pattern appears in less obvious forms: two functions that call each other (parseValue → parseArray → parseValue), or a recursive call that forgets to shrink the problem, such as passing the same range to a quicksort partition. In a backtrace, infinite recursion is unmistakable: thousands of identical frames. (factorial also overflows int beyond n = 12, a separate bug that the stack will not tell you about.)

Large local arrays

void process() {
    int bigArray[1000000]; // ~4 MB on stack—may overflow
}

Fix:

void process() {
    std::vector<int> bigArray(1000000);
}

This one crashes on entry to the function, before any line of its body runs, because the whole frame is reserved at once. In a debugger the crash appears on the opening brace or on a stack probe helper (__chkstk on Windows, or the probe loop GCC emits with -fstack-clash-protection), which confuses people looking for a bad line. On Windows the 1 MB default makes this especially easy to hit: a char buffer[2 * 1024 * 1024] for “file reading” is enough. std::vector keeps only three pointers on the stack and puts the elements on the heap. The same applies to large std::array members: a struct containing std::array<int, 1000000> declared as a local is just as bad as the raw array.

Very deep naive recursion (e.g. Fibonacci)

Fix: Iteration or memoization.

The naive fib(n) = fib(n-1) + fib(n-2) is mostly a time problem (exponential calls) rather than a depth problem, since its depth is only n. The stack becomes the issue in algorithms whose depth is proportional to the input size: recursive traversal of a linked list, depth-first search over a graph shaped like a long path, a recursive flood fill on a large image, or quicksort that always picks a bad pivot. A flood fill on a 2000 x 2000 region can recurse millions of levels deep, which no default stack survives.

Deep call chains with large frames

Move big temporaries to the heap or reuse buffers.

Depth and frame size multiply. A recursive function with a 4 KB local buffer overflows an 8 MB stack at about 2,000 levels, while the same function without the buffer might reach hundreds of thousands. GCC and Clang can report frame sizes: -fstack-usage writes a .su file per translation unit listing each function’s frame, and -Wframe-larger-than=8192 warns about any function whose frame exceeds 8 KB. MSVC has warning C6262 in code analysis for large frames.


Limiting recursion depth

int factorial(int n, int depth = 0) {
    const int MAX_DEPTH = 1000;
    if (depth > MAX_DEPTH) {
        throw std::runtime_error("Recursion too deep");
    }
    if (n <= 1) return 1;
    return n * factorial(n - 1, depth + 1);
}

Prefer loops when depth can be large.

A depth guard turns a crash into an error you can handle, which matters when depth comes from input: a parser that receives [[[[...]]]] nested a million levels deep should reject the document, not crash the server. That is why most JSON libraries have a maximum nesting depth. The limit must be well below what the stack can actually hold in the worst build configuration (debug, sanitizers, a smaller thread stack), so pick it conservatively. For factorial the guard is illustrative only, since n itself bounds the depth; it belongs in functions whose depth you cannot predict from the arguments.


Stack size

Linux: ulimit

ulimit -s       # size in KB
ulimit -s 16384 # e.g. 16 MB
ulimit -s unlimited  # use with care

Windows linker

Linker → System → Stack Reserve (Visual Studio), or:

/STACK:16777216

CMake (examples)

# Linux ELF (toolchain-specific)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,stack-size=16777216")
# MSVC
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:16777216")

Where the setting lives differs by platform, and that causes confusion. On Linux, the main thread’s stack size is decided by the resource limit (ulimit -s) of the shell or service manager that starts the process, not by the binary; ulimit changes only the current shell and its children, and for a systemd service you set LimitSTACK=. The -z stack-size linker flag does not change the main thread’s stack on glibc systems (musl uses it as the default for new threads). On Windows, /STACK is written into the executable header and applies to the main thread and to threads created without an explicit size. In practice, the most portable way to get a big stack for a deep algorithm is to run that algorithm in a thread you create with a chosen size (pthread_attr_setstacksize, CreateThread, or boost::thread::attributes).

I treat a larger stack as a legitimate fix only when I know the maximum depth, for example a compiler pass over an AST whose depth is bounded by a language rule. For depth that users control, it just moves the crash to a larger input.


Heap instead of stack

void process() {
    std::vector<int> big(1000000);
    // or
    auto big = std::make_unique<int[]>(1000000);
}

The two lines in process() are alternatives (declaring big twice would not compile). std::vector is the default; std::make_unique<int[]> avoids the vector’s size and capacity bookkeeping and is useful when the size is fixed. Note that make_unique<int[]>(n) value-initializes, so the ints are zeroed; C++20’s make_unique_for_overwrite skips that when you are about to fill the buffer anyway. A static local array also avoids the stack, but makes the function non-reentrant and not thread-safe, so it is rarely the right fix.

Tree traversal: explicit std::stack

Replace deep recursion with an iterative walk using a container on the heap when depth is unbounded (e.g. deeply nested JSON).

#include <stack>

struct Node {
    int value;
    std::vector<Node*> children;
};

// Recursive: depth of the tree = depth of the call stack
void visitRecursive(Node* n) {
    if (!n) return;
    process(n->value);
    for (Node* c : n->children) visitRecursive(c);
}

// Iterative: depth of the tree = size of a heap-allocated container
void visitIterative(Node* root) {
    std::stack<Node*> pending;
    if (root) pending.push(root);
    while (!pending.empty()) {
        Node* n = pending.top();
        pending.pop();
        process(n->value);
        // push in reverse so children are visited left to right
        for (auto it = n->children.rbegin(); it != n->children.rend(); ++it)
            pending.push(*it);
    }
}

The iterative version does the same pre-order walk, but each pending node costs one pointer in a std::deque on the heap instead of a whole stack frame, and the heap can grow to gigabytes. The price is readability: in-order and post-order traversals, or algorithms that need to do work after each child returns, require storing extra state per entry (for example a pair of node and child index). A related trap is the destructor of a long linked structure: std::unique_ptr<Node> next destroys the list recursively, so dropping a list of a million nodes overflows the stack in the destructor. Unlink nodes in a loop inside ~List() to avoid it.


Tail recursion

Non-tail (work after the call):

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

Tail form (call is last):

int factorialTail(int n, int acc = 1) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);
}

Inspect assembly at -O2 to see if the compiler optimized to a loop.

In factorialTail, nothing is left to do after the recursive call returns, so the compiler can reuse the current frame and jump instead of calling. GCC and Clang do this reliably at -O2, MSVC in release builds often does, and no compiler does it at -O0. C++ does not guarantee tail-call elimination, so code that depends on it for correctness is fragile: it works in release, overflows in debug, and can silently stop being optimized when someone adds a local object with a destructor (the destructor must run after the call, so the call is no longer in tail position). Clang offers [[clang::musttail]] to make the requirement explicit and turn a missed optimization into a compile error. For portable code, if you have already rewritten a function into tail form, converting it into a while loop is one more small step and removes the dependency on the optimizer.


Case studies (sketches)

JSON parsing

Deep nesting can blow the stack with naive recursion; use an explicit stack or iteration.

Recursive-descent parsers are the classic real-world case, because the input decides the depth. A few kilobytes of [ characters are enough to crash a naive parser, which makes it a denial-of-service issue for servers that parse untrusted JSON, XML or YAML. The usual defenses are a nesting-depth limit (the simplest, and what most parsers offer as an option), or an iterative parser that keeps its own stack of open containers.

Directory walk

Replace deep filesystem recursion with a directory stack/std::stack of paths.

std::filesystem::recursive_directory_iterator already works this way and keeps its state on the heap, so it is usually better than a hand-written recursive walk. Deep directory trees are rare, but symbolic link loops are not; the iterator does not follow directory symlinks unless you pass directory_options::follow_directory_symlink, which is the setting that prevents infinite walks.


Summary

Checklist

  • Base case for every recursive function?
  • Avoid multi-megabyte stack arrays?
  • Recursion depth bounded or converted to iteration?
  • Nested calls not multiplied by huge locals?

Mitigation priority

  1. Correct base cases
  2. Heap for large buffers
  3. Iterative algorithms / explicit stacks
  4. Tail recursion where applicable
  5. Increase stack only as a temporary measure

Rules

  1. Large arrays → vector / unique_ptr
  2. Guard recursion depth when needed
  3. Deep or unbounded recursion → iteration
  4. Consider tail-call patterns with optimizations enabled

Investigating a stack overflow

When a crash report arrives, I start with a backtrace. In gdb, bt on a stack overflow may print tens of thousands of frames, so use bt 20 and bt -20 to see the top and the bottom: repeated identical frames mean recursion, while a short backtrace ending in a function with a large frame means a big local. info frame shows the current frame’s address, and comparing $sp in two frames tells you how much stack each level uses. AddressSanitizer (-fsanitize=address) catches the overflow and prints ERROR: AddressSanitizer: stack-overflow on address ... with a symbolized stack, which is often the quickest confirmation. Valgrind is less helpful here, since its own default stack limits differ from the native run.

To reproduce a borderline case reliably, lower the limit (ulimit -s 1024) instead of trying to generate huge inputs; if the program crashes at 1 MB and not at 8 MB, the problem is depth or frame size, not memory corruption.


Closing

Stack overflow is preventable: control recursion, avoid huge stack frames, and use the heap for large data. Increasing stack size is a last resort; fix the algorithm and memory placement first.


Frequently Asked Questions (FAQ)

Q. Why does my code overflow the stack only in a worker thread?

A. Secondary threads often get less stack than the main thread. On macOS non-main threads default to 512 KB versus 8 MB for the main thread, and musl-based systems such as Alpine default to a small thread stack as well. std::thread cannot set a stack size, so either move the large buffers or deep recursion to the heap, or create the thread with a platform API (pthread_attr_setstacksize, or the stack size argument of CreateThread).