root.system / 0x14 / recursion

A function that calls itself.
Until it doesn't.

Recursion is the most elegant idea in programming. It is also the fastest way to destroy a program. One missing line of code, the stack overflows, the process dies. This page is about that line, and why it is the only thing standing between beautiful code and a crashed server.

Beginner// level 01

What recursion actually is

Your program just killed itself. Not slowly. Not with a warning. Not with a chance to recover. Instantly.

One function called itself. That function called itself. That function called itself. Until there was nothing left. This is recursion, and in the wrong hands it is the most elegant way to destroy a program ever invented.

You already know the call stack. You learned it on the memory page. Every function call pushes a frame: local variables, return address, the pointer back to where to go when the function finishes. The stack is fast. The stack is clean. The stack is finite. That last part is the problem.

A recursive function solves a problem by solving a smaller version of the same problem, until the problem is so small it solves itself. That stopping point has a name: the base case. Without it, the function never stops, the stack never stops growing, and the OS terminates your process.

the base case
The exit condition
The moment the function stops calling itself: the answer it already knows. Without it: infinite recursion, stack overflow, a dead program. Example: factorial(0) = 1. Zero has no factorial to calculate. It just is 1.
the recursive case
The step that makes progress
Each call must be smaller than the last, closer to the base case, moving in one direction. Never sideways, never backwards. Example: factorial(n) = n × factorial(n-1). n-1 is smaller, so it eventually reaches 0 and stops.

The difference is one line

WITH a base case:
factorial(5)
  factorial(4)
    factorial(3)
      factorial(2)
        factorial(1)
          factorial(0)  ← BASE CASE: returns 1
        returns 1
      returns 2
    returns 6
  returns 24
returns 120

WITHOUT a base case:
recurse(1)
  recurse(2)
    recurse(3)
      recurse(4)
        ...
          recurse()
            💥 STACK OVERFLOW

The wrong way and the right way

Rust• • •
// THE WRONG WAY: no base case. This will crash, every time.
// The stack has a limit (usually 8MB on Linux). Each frame is
// maybe 48 bytes: roughly 174,000 calls before death.
fn recurse_wrong(n: u32) {
    println!("{}", n);
    recurse_wrong(n + 1); // calls itself forever
                          // no exit condition
                          // stack grows until the OS kills it
}
// thread 'main' has overflowed its stack
// fatal runtime error: stack overflow

// THE RIGHT WAY: base case first, always.
fn factorial(n: u64) -> u64 {
    match n {
        0 => 1,                    // base case: you know the answer
        n => n * factorial(n - 1), // recursive case: n-1 is smaller
    }
    // match is exhaustive; the compiler verifies every case is
    // handled. You cannot forget the base case and still compile.
}

fn main() {
    println!("{}", factorial(10)); // 3628800
    println!("{}", factorial(0));  // 1, the base case
    println!("{}", factorial(1));  // 1, one step from the base case
}
C• • •
#include <stdint.h>
#include <stdio.h>

/* THE WRONG WAY: no base case. This will crash, every time. */
void recurse_wrong(int n) {
    printf("%d\n", n);
    recurse_wrong(n + 1); /* no base case */
                          /* no exit */
                          /* stack overflow incoming */
}
/* Segmentation fault (core dumped) */

/* THE RIGHT WAY: base case first, always. */
uint64_t factorial(uint64_t n) {
    if (n == 0) return 1;         /* base case */
                                  /* without this line, */
                                  /* nothing ever returns */
    return n * factorial(n - 1);  /* recursive case */
}
/* C trusts you to write the base case. */
/* The compiler will not remind you. The crash will. */

int main(void) {
    printf("%llu\n", factorial(10)); /* 3628800 */
    printf("%llu\n", factorial(0));  /* 1 */
    return 0;
}

Watch the stack grow and shrink

This is recursion made visible. Pick a function, choose an n, and step through it. Each call pushes a frame; the green base case is where it finally stops; each return pops a frame back off. Then flip danger mode to remove the base case and watch the stack climb until it overflows.

// watch the stack grow and shrink

step through the calls, or hit run.

call stack · depth 1
factorial(5)CALLING
execution log
factorial(5) called

pick a function and an n, then step through it. each call pushes a frame; the green base case is where it finally stops; returns pop the frames back off. flip danger mode to remove the base case and watch the stack climb to overflow.

Intermediate// level 02

The stack, the heap, and the cost

Every recursive call has a cost: a stack frame holding local variables, the return address, and parameters. On a 64-bit system that is maybe 48 to 200 bytes each. The OS gives your stack roughly 8 megabytes, so that is between 40,000 and 170,000 frames. Deep enough for most problems. Not deep enough for all of them. This is where recursion becomes dangerous.

recursive · the stack
Fast, automatic, finite
Fast to push and pop, automatic cleanup on return, cache-friendly because frames are adjacent. But a hard limit of ~8MB on Linux. n = 100,000 is likely a stack overflow.
iterative · the heap
Verbose, manual, vast
The heap is gigabytes. No automatic cleanup, slightly more code, an explicit stack or accumulator. But n = 10,000,000 is no problem. Always safer for deep problems.
// the critical insight
The recursive concept can be beautiful. The recursive implementation can be fatal. Knowing which to use is the scar that separates junior from senior.

Recursion on a recursive data structure

A linked list is itself a recursive structure (each node points at the next), so recursive traversal feels natural. It is also a trap: one frame per node means a long list overflows. The iterative version does the same work in a single frame. You learned both structures on the linked-list page; here is the cost of choosing recursion over iteration on them.

Rust• • •
// Recursive linked-list traversal. Natural. Clean. Dangerous at scale.
fn sum_list(node: Option<&Node>) -> i64 {
    match node {
        None => 0, // base case: empty list
        Some(n) => n.value + sum_list(n.next.as_deref()),
        // one stack frame per node
        // a list of 100,000 nodes = 100,000 frames = likely overflow
    }
}

// Iterative version: same result, no stack risk.
fn sum_list_iter(mut node: Option<&Node>) -> i64 {
    let mut total = 0;
    while let Some(n) = node {
        total += n.value;       // accumulate in one frame
        node = n.next.as_deref();
    }
    total
    // a list of 100,000,000 nodes = no problem
}
C• • •
#include <stdint.h>

/* Recursive: natural but dangerous at scale. */
int64_t sum_list(const Node *node) {
    if (node == NULL) return 0;   /* base case */
    return node->value + sum_list(node->next);
    /* one frame per node */
    /* 100,000 nodes = 100,000 frames = probably a crash */
}

/* Iterative: verbose but safe, always. */
int64_t sum_list_iter(const Node *node) {
    int64_t total = 0;
    while (node != NULL) {
        total += node->value;
        node = node->next;
    }
    return total;
    /* one frame, any list length, never crashes */
}

The Fibonacci trap

The naive recursive Fibonacci is the classic cautionary tale. It is not the stack that kills it but the branching: two calls per invocation means fib(50) spawns roughly a quadrillion calls and never finishes. Memoisation (a hash map, from the hashing page) caches each result so every value is computed once.

Rust• • •
// Naive fibonacci: beautiful, catastrophically slow.
fn fib_naive(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        n => fib_naive(n - 1) + fib_naive(n - 2),
        // two recursive calls per invocation
        // fib(10) makes 177 calls
        // fib(50) makes ~2^50 calls (about a quadrillion)
        // this never finishes for large n
    }
}

// Memoised fibonacci: same concept, now usable.
use std::collections::HashMap;

fn fib_memo(n: u64, memo: &mut HashMap<u64, u64>) -> u64 {
    if let Some(&cached) = memo.get(&n) {
        return cached; // already computed
    }
    let result = match n {
        0 => 0,
        1 => 1,
        n => fib_memo(n - 1, memo) + fib_memo(n - 2, memo),
    };
    memo.insert(n, result); // store for reuse
    result
    // fib(50): 99 calls instead of a quadrillion
}
C• • •
#include <stdint.h>

/* Naive fibonacci: exponential time. */
uint64_t fib_naive(uint64_t n) {
    if (n <= 1) return n;
    return fib_naive(n - 1) + fib_naive(n - 2);
    /* two recursive calls: O(2^n) time */
    /* fib(50) never finishes in practice */
}

/* Memoised fibonacci: linear time. */
static uint64_t cache[100]  = {0};
static int      cached[100] = {0};

uint64_t fib_memo(uint64_t n) {
    if (n <= 1) return n;
    if (cached[n]) return cache[n];
    cache[n]  = fib_memo(n - 1) + fib_memo(n - 2);
    cached[n] = 1;
    return cache[n];
    /* each value computed once: O(n) time */
}
// two different failures
sum_list fails on depth: too many frames. fib_naive fails on breadth: too many calls. Recursion can blow up in both directions, and the fixes are different: iteration for depth, memoisation for breadth.
Advanced// level 03

Tail recursion, trampolining, and the third option

There is a third choice between beautiful-but-dangerous recursion and safe-but-ugly iteration: tail recursion. When the compiler supports it, the stack never grows at all.

A call is tail-recursive when the recursive call is the last operation: nothing happens after it returns, so the current frame has no more work to do, so the compiler can reuse it. Stack depth stays constant. Recursion depth becomes unlimited. At the machine-code level it is identical to a loop.

Rust• • •
// NOT tail recursive: the stack grows with n.
fn sum_wrong(n: u64) -> u64 {
    if n == 0 { return 0; }
    n + sum_wrong(n - 1)
    // the addition happens AFTER the call returns,
    // so this frame must stay on the stack to finish.
    // n = 100,000 -> 100,000 frames -> overflow
}

// Tail recursive: the stack stays constant.
fn sum_tail(n: u64, acc: u64) -> u64 {
    if n == 0 { return acc; }
    sum_tail(n - 1, acc + n)
    // the addition happens BEFORE the call.
    // nothing is left to do after, so the compiler
    // can reuse the frame: this becomes a loop.
}

fn sum(n: u64) -> u64 {
    sum_tail(n, 0) // public API hides the accumulator
}

// Rust does not guarantee tail-call optimisation, but LLVM
// often applies it in release builds. For a guarantee, use a
// loop or the trampoline pattern below.
C• • •
#include <stdint.h>

/* NOT tail recursive. */
uint64_t sum_wrong(uint64_t n) {
    if (n == 0) return 0;
    return n + sum_wrong(n - 1); /* add after the call returns */
                                 /* so the frame stays alive */
}

/* Tail recursive: GCC and Clang optimise this with -O2. */
uint64_t sum_tail(uint64_t n, uint64_t acc) {
    if (n == 0) return acc;
    return sum_tail(n - 1, acc + n); /* the last operation */
                                     /* becomes a jmp, not a call */
}

uint64_t sum(uint64_t n) {
    return sum_tail(n, 0);
}

/* Verify the tail call was optimised:
 *   gcc -O2 -S recursion.c
 * Look for 'jmp' instead of 'call' in the output. */

The trampoline pattern

When tail-call optimisation is not guaranteed (Rust does not promise it), a trampoline gives you the same constant-stack behaviour by hand: the recursive function returns a description of the next call instead of making it, and a plain loop drives it forward. Recursion in concept, iteration in execution.

Rust• • •
// A trampoline turns recursion into iteration, guaranteed,
// regardless of compiler support.
enum Bounce<T> {
    Done(T),
    More(Box<dyn FnOnce() -> Bounce<T>>),
}

fn trampoline<T>(mut bounce: Bounce<T>) -> T {
    loop {
        match bounce {
            Bounce::Done(val) => return val,
            Bounce::More(f) => bounce = f(),
            // each iteration replaces the closure.
            // no stack growth whatsoever.
        }
    }
}

fn fact_bounce(n: u64, acc: u64) -> Bounce<u64> {
    if n == 0 {
        Bounce::Done(acc)
    } else {
        Bounce::More(Box::new(move || fact_bounce(n - 1, acc * n)))
    }
}

fn factorial(n: u64) -> u64 {
    trampoline(fact_bounce(n, 1))
    // runs as a loop internally: zero stack growth, any n.
}
C• • •
#include <stdint.h>

/* A trampoline in C using a small state struct. */
typedef struct Bounce {
    int      done;
    uint64_t value;
    uint64_t next_n;
    uint64_t next_acc;
} Bounce;

Bounce fact_bounce(uint64_t n, uint64_t acc) {
    if (n == 0) {
        return (Bounce){ .done = 1, .value = acc };
    }
    return (Bounce){
        .done     = 0,
        .next_n   = n - 1,
        .next_acc = acc * n,
    };
}

uint64_t factorial(uint64_t n) {
    Bounce b = fact_bounce(n, 1);
    while (!b.done) {                 /* iteration, not recursion */
        b = fact_bounce(b.next_n, b.next_acc);
    }
    return b.value;
    /* one frame, any n, no stack growth */
}

Mutual recursion: the hidden version

Recursion does not have to be a function calling itself directly. Two functions calling each other create the exact same stack risk, and it is easier to miss in a review because no single function looks recursive.

Rust• • •
// Mutual recursion: two functions calling each other.
// Same stack risk as direct recursion.
fn is_even(n: u64) -> bool {
    if n == 0 { return true; }
    is_odd(n - 1) // calls is_odd
}

fn is_odd(n: u64) -> bool {
    if n == 0 { return false; }
    is_even(n - 1) // calls is_even
    // is_even -> is_odd -> is_even -> is_odd ...
    // n = 100,000 -> 100,000 frames -> overflow
}

// Safe version: one function, no mutual calls.
fn is_even_safe(n: u64) -> bool {
    n % 2 == 0 // O(1). No recursion. No stack.
}
C• • •
#include <stdbool.h>
#include <stdint.h>

/* Mutual recursion: same stack risk as direct recursion. */
bool is_odd(uint64_t n);

bool is_even(uint64_t n) {
    if (n == 0) return true;
    return is_odd(n - 1);   /* calls is_odd */
}

bool is_odd(uint64_t n) {
    if (n == 0) return false;
    return is_even(n - 1);  /* calls is_even */
    /* is_even -> is_odd -> ... -> n frames -> overflow */
}

/* Safe version: no recursion, no stack. */
bool is_even_safe(uint64_t n) {
    return n % 2 == 0; /* O(1) */
}

Recursion in Bitcoin, and why Bitcoin avoids it

The blockchain is conceptually recursive. Is block N valid? Only if block N-1 is valid. Only if block N-2 is valid. All the way back to block 0, the genesis block. This is recursive validation, and it is beautiful in concept.

But the Bitcoin chain is more than 800,000 blocks deep. A recursive validator would need 800,000 stack frames. At ~200 bytes each that is 160 megabytes of stack against an 8 megabyte limit. Stack overflow, on every machine, every time. So Bitcoin Core uses iteration.

Rust• • •
// The blockchain is conceptually recursive: block N is valid
// only if block N-1 is valid, all the way back to genesis.
// But the chain is 800,000+ blocks deep. Recursion would need
// 800,000 frames and overflow on every machine. So: iterate.
fn validate_chain(tip: &Block) -> bool {
    let mut current = Some(tip);
    while let Some(block) = current {
        if !validate_block(block) {
            return false;
        }
        current = block.prev.as_deref(); // iterate backward
    }
    true
    // the recursive thinking is preserved.
    // the recursive implementation is forbidden.
}
C• • •
/* Bitcoin Core validates the chain iteratively, never
 * recursively. 800,000 blocks at ~200 bytes per frame would be
 * 160MB of stack against an 8MB limit: instant overflow. */
void validate_chain(Block *tip) {
    Block *current = tip;
    while (current != NULL) {
        if (!validate_block(current)) {
            reject_chain();
            return;
        }
        current = current->prev; /* iterate backward */
    }
    /* one stack frame, 800,000 blocks, no overflow, ever */
}

The Merkle tree is validated iteratively too, even though trees are naturally recursive structures. Cryptographic code avoids recursion for three reasons:

  • Stack depth is unpredictable. Inputs from untrusted sources can be crafted to maximise depth. A malicious transaction could push a recursive validator to overflow: a denial-of-service attack.
  • Constant stack usage is a security property. SHA-256 uses exactly the same stack space regardless of input. Attackers cannot influence resource usage through input size.
  • The recursive concept survives as documentation. The reasoning stays recursive; the implementation that ships is iterative.
// the wisdom recursion teaches
The concept can be recursive. The code must be safe. The blockchain page showed the chain as a linked list of hash-linked blocks; this is why that list is always walked with a loop, never a recursive descent. A trustless network cannot let an attacker choose how deep your stack goes.

Where recursion touches BitRoot

Recursion is a technique, not a layer, so it reaches into nearly every page below it:

0x06 / memory
The stack has a limit
Every recursive call pushes a frame onto the stack, a region of memory with a hard ~8MB limit. Recursion without a base case exhausts it and kills the process.
0x05 / cpu
The stack pointer
The CPU's stack pointer decrements on every call and increments on every return. Cross the stack boundary and the OS sends SIGSEGV. Recursion is the CPU updating one register and writing memory.
0x07 / operating system
The OS sets the limit
8MB on Linux and macOS, 1MB on Windows by default. You can raise it with ulimit, but you cannot make it infinite. The OS enforces the physical boundary.
0x09 / pointers
Return addresses
The return address on each frame is a pointer to the instruction to run after the function returns. Deep stacks mean many of them, and buffer-overflow and ROP attacks target exactly these.
0x0B / arrays
Divide and conquer
Binary search, merge sort, and quicksort are recursive in concept, each with a careful base case that prevents infinite descent. The foundation of algorithms.
0x0C / linked lists
A recursive structure
Each node contains a next pointer, so traversal is naturally recursive and dangerously so: ten million nodes need ten million frames recursively, one frame iteratively.
0x0D / hashing
Constant depth on purpose
SHA-256 uses the same stack depth regardless of input. Cryptographic functions avoid recursion because variable, attacker-influenced depth is a vulnerability.
0x0A / compile vs runtime
A runtime failure
Rust warns about infinite recursion at compile time when it can detect it, but most recursive bugs surface at runtime: the overflow happens when the program runs, not when it compiles.
0x13 / blockchain
Validated with a loop
Bitcoin validates 800,000+ blocks iteratively. The chain is conceptually recursive (this block is valid because the previous is) but the implementation is a while loop, because recursion at that depth would overflow every node.
0x10 / distributed systems
A DoS surface
Recursive algorithms on data from untrusted peers are dangerous: a malicious peer can craft a structure that maximises recursive depth and triggers a stack overflow. Iterative validation defends against it.
next up / 0x15
Same answer, 317 years apart. How to measure the shape of an algorithm.
big o notation