Recursion in PHP: The Trampoline Pattern Against Stack Overflow Errors
AI generated
<?php
8.4
PHP · Functional Programming · Recursion
Recursion in PHP
The Trampoline Pattern Against Stack Overflow Errors

PHP does not optimize away tail recursion, which is why deeply recursive functions can crash with a stack overflow on large inputs. The trampoline pattern solves this problem by turning recursion into a safe, iterative loop, without giving up the recursive programming style itself.

18 min read Trampoline · Tail Recursion · Stack Safety PHP 8.2 · 8.3 · 8.4

1. Why recursion in PHP hits its limits

Recursion is a core tool of functional programming: a function solves a problem by calling itself with a smaller version of the same problem until a base case is reached. Tree traversal, processing nested data structures, and many mathematical definitions can often be expressed more clearly with recursion than with a manually written loop, because the structure of the code directly mirrors the structure of the problem.

The practical problem in PHP: every recursive call occupies a new stack frame in the PHP interpreter's call stack, and that stack has a limited size. At a recursion depth of a few thousand to several tens of thousands of calls, depending on system configuration and available memory, PHP aborts with a fatal error due to stack overflow. A mathematically elegant recursive function thus becomes unusable in practice as soon as the input size crosses a certain threshold.

The trampoline pattern solves exactly this problem by turning deeply nested recursion into a flat, iterative loop, without fully giving up the recursive programming style and its clarity. This article shows why PHP has no tail call optimization, how a trampoline is concretely implemented, and where the practical limits of this technique lie.

2. Tail recursion and why PHP does not optimize it

A recursive call counts as a tail call when it is the last operation within a function and the return value is taken directly from the recursive call, without anything further happening to the result afterward. In languages with tail call optimization, such as Scheme or Erlang, the interpreter or compiler recognizes this pattern and replaces the current stack frame with the new one, instead of stacking an additional frame on top. This keeps memory usage constant for tail recursion, regardless of recursion depth.

PHP does not perform this optimization, neither the Zend interpreter nor the JIT compiler in PHP 8.4 recognize or optimize tail calls. Every recursive call, whether a tail call or not, occupies its own stack frame, which is only released after the call fully returns. This language decision has also not been announced for future PHP versions, which is why recursion in PHP must fundamentally be considered limited in its maximum depth, regardless of how cleanly the tail call is formulated.


<?php

declare(strict_types=1);

// This looks like a tail call, but PHP does NOT optimize it away
function sumRecursive(int $n, int $accumulator = 0): int
{
    if ($n === 0) {
        return $accumulator;
    }

    // Tail position, but still consumes a new stack frame in PHP
    return sumRecursive($n - 1, $accumulator + $n);
}

echo sumRecursive(1000); // works fine

// echo sumRecursive(1_000_000);
// Fatal error: Allowed memory size exhausted / stack overflow,
// depending on system stack size configuration

3. Provoking and measuring the stack overflow in practice

To observe this behavior concretely, the maximum recursion depth can be determined experimentally by calling a harmless recursive function with growing depth until PHP aborts with an error. This limit varies depending on operating system, PHP configuration, and memory available to the process, but typically lies somewhere between a few thousand and several tens of thousands of calls under default settings.

In practice, this limit is mostly encountered when recursively processing tree structures with unexpectedly large depth, for example parsing nested JSON documents of unknown origin, traversing a very deep category hierarchy in a shop system, or running recursive algorithms on large, user-generated datasets. The error often only surfaces in production with real data, while test data is usually too small to reach the limit.


<?php

declare(strict_types=1);

function countDepth(int $depth = 0): int
{
    try {
        return countDepth($depth + 1);
    } catch (\Error $e) {
        // In practice a stack overflow crashes the process before
        // this catch is reached — this illustrates the concept only
        return $depth;
    }
}

// A safer experiment: cap the depth explicitly and observe memory
function recurseUpTo(int $target, int $current = 0): int
{
    if ($current >= $target) {
        return $current;
    }

    return recurseUpTo($target, $current + 1);
}

echo recurseUpTo(5000);   // typically fine
// echo recurseUpTo(500000); // typically crashes on default configurations

4. The core idea of the trampoline pattern

The core idea of the trampoline pattern is to have a recursive function not call itself directly, but instead return a description of the next step as a closure. An outer loop, the actual trampoline, repeatedly calls this closure as long as the result is another closure, instead of a finished value. Only once a concrete value comes back instead of a closure is the computation complete.

The decisive effect: because the recursive function itself never calls itself directly, but only returns a closure, every call returns immediately, and its stack frame is released right away. The actual repetition happens in the outer loop, whose stack depth stays constant regardless of how many logical recursion steps are executed in total. The trampoline pattern thus simulates tail call optimization at the application level, because PHP does not perform it itself.

5. Building your own trampoline() function

A minimal trampoline function takes a closure, calls it in a loop, and checks after every call whether the result is a closure again. If it is a closure, it gets called again on the next loop iteration. If it is no longer a closure but a concrete value, the loop ends and the value is returned. This simple structure is enough to execute arbitrarily deep logical recursion with constant stack usage.

For type safety, it matters to clearly declare the return type of the trampolined function: either a new closure for the next step, or the final result value. In PHP this can be modeled with a union type Closure|mixed, or pragmatically with mixed and an instanceof Closure check inside the trampoline loop.


<?php

declare(strict_types=1);

/**
 * Runs a "bouncing" computation until it stops returning a Closure.
 * Keeps stack depth constant regardless of logical recursion depth.
 */
function trampoline(Closure $fn, mixed ...$args): mixed
{
    $result = $fn(...$args);

    while ($result instanceof Closure) {
        $result = $result();
    }

    return $result;
}

/**
 * Instead of calling itself directly, returns a Closure describing
 * the next step. No new stack frame accumulates across iterations.
 */
function sumTrampolined(int $n, int $accumulator = 0): Closure|int
{
    if ($n === 0) {
        return $accumulator;
    }

    return fn (): Closure|int => sumTrampolined($n - 1, $accumulator + $n);
}

echo trampoline(sumTrampolined(...), 1_000_000); // works, constant stack depth

6. Rewriting recursive functions for the trampoline

To make an existing recursive function usable for the trampoline pattern, it must first be brought into tail recursive form, meaning it needs an accumulator parameter that carries the running intermediate result, instead of processing the result further after the recursive call returns. A function such as return n + factorial(n - 1), which still performs a multiplication after the recursive call, is not tail recursive and must first be rewritten before it is suitable for the trampoline.

After this transformation, the direct recursive call is replaced by a closure describing the next step, as shown in the previous section. This restructuring costs a bit of clarity compared to the original, direct recursion, but gains safety against stack overflow for arbitrarily deep inputs, a trade-off that pays off for functions whose input depth cannot be safely bounded at design time.


<?php

declare(strict_types=1);

// Not tail-recursive: multiplication happens AFTER the recursive call returns
function factorialNaive(int $n): int
{
    if ($n <= 1) {
        return 1;
    }

    return $n * factorialNaive($n - 1); // multiplication after the call
}

// Tail-recursive form: accumulator carries the running product
function factorialTrampolined(int $n, int $accumulator = 1): Closure|int
{
    if ($n <= 1) {
        return $accumulator;
    }

    // The recursive step is now the last operation, described as a Closure
    return fn (): Closure|int => factorialTrampolined($n - 1, $accumulator * $n);
}

echo trampoline(factorialTrampolined(...), 20);     // safe, small input
echo trampoline(factorialTrampolined(...), 10_000); // safe, constant stack usage

7. Resolving mutual recursion with the trampoline

Another strong use case for the trampoline pattern is mutual recursion, where two functions call each other alternately, for example an isEven function that relies on isOdd, and vice versa. Without a trampoline, every switch between the two functions would occupy another stack frame, causing mutual recursion to hit the stack limit even faster than simple self-recursion.

With the trampoline, both functions return a closure referring to the other function instead of calling it directly. The outer trampoline loop takes care of the actual execution, regardless of which of the two functions is up next. This pattern works for any number of mutually calling functions, not just pairs.


<?php

declare(strict_types=1);

function isEvenTrampolined(int $n): Closure|bool
{
    if ($n === 0) {
        return true;
    }

    return fn (): Closure|bool => isOddTrampolined($n - 1);
}

function isOddTrampolined(int $n): Closure|bool
{
    if ($n === 0) {
        return false;
    }

    return fn (): Closure|bool => isEvenTrampolined($n - 1);
}

var_dump(trampoline(isEvenTrampolined(...), 100_000)); // true, no stack overflow

8. Limits: when a simple loop is the better choice

The trampoline pattern solves the stack overflow problem, but also introduces noticeable overhead: every iteration creates a new closure that the garbage collector must clean up again, and the additional indirection through instanceof Closure checks costs measurable runtime compared to a direct, classic for or while loop. For simple, clearly structured iterations such as summing an array, an ordinary loop is almost always the simpler and faster solution.

The trampoline pattern pays off where the recursive programming style itself brings a genuine clarity advantage, for example with complex state machines or parsers using mutual recursion, whose direct conversion into a loop would make the code considerably harder to follow. For a simple linear iteration over known, bounded amounts of data, on the other hand, a classic loop is both faster and easier to understand than a trampoline.

9. Recursion strategies in direct comparison

The following table compares direct recursion, the trampoline pattern, and the classic loop.

Criterion Direct Recursion Trampoline Pattern Classic Loop
Stack safety at large depth Risk beyond a few thousand levels Constant stack, any depth Constant stack, any depth
Readability for recursive structures Very clear Extra closure indirection Often messier for trees
Performance overhead Low at shallow depth Closure allocation per step Minimal
Mutual recursion Hits the limit faster Well supported Must be rebuilt manually
Suited for simple linear iteration Unnecessary overhead Unnecessary overhead First choice

The rule of thumb: as long as the maximum recursion depth can be safely bounded at design time, direct recursion remains the clearest solution. Once the input depth depends on external, uncontrolled data, such as with recursive parsing, the trampoline pattern is the more robust choice, while simple linear iterations are almost always best solved with a classic loop.

Mironsoft

PHP architecture, code reviews and robust recursion in everyday team work

Stack overflow errors from deep recursion in your code?

We identify recursive functions with unclear input depth and show where the trampoline pattern reliably eliminates stack overflow risk, without sacrificing the clarity of recursion.

Code Review

Analysis for recursive functions with unbounded or unclear depth

Refactoring

Safely migrating critical recursion onto the trampoline pattern

Training

Teaching recursion patterns and stack limits hands on within the team

10. Summary

PHP performs no tail call optimization, which is why deeply recursive functions can crash with a stack overflow on large inputs, even when the recursive call is formally in tail position. The trampoline pattern solves this problem by having a recursive function return a closure for the next step instead of a direct call, which is repeatedly invoked by an outer, stack-neutral loop until a concrete value is available.

This technique also works for mutual recursion between several functions and keeps stack usage constant regardless of logical recursion depth. The price is additional overhead from closure allocation and indirection, which is why the trampoline pattern pays off specifically for cases with unclear or potentially very large input depth, not as a generic replacement for simple, clearly bounded loops.

Recursion and the Trampoline Pattern in PHP — The Key Takeaways

Problem

PHP does not optimize tail calls, every recursive call occupies its own stack frame.

Trampoline Solution

The function returns a closure for the next step, an outer loop runs it stack-neutrally.

Mutual Recursion

Several alternating functions can be run safely through the same trampoline.

Limits

Closure overhead usually makes the pattern unnecessary for simple linear iteration, a classic loop suffices.

11. FAQ: Recursion and the Trampoline Pattern in PHP

1Why can recursion cause a stack overflow?
Every call occupies a new stack frame released only after full return, exceeding the limited stack at deep recursion.
2Does PHP optimize tail recursion?
No, neither the Zend interpreter nor the JIT recognize tail calls, every call consumes its own stack frame.
3What is the trampoline pattern?
A function returns a closure for the next step instead of a direct call, an outer loop runs it repeatedly.
4How does the stack stay constant?
Every call returns immediately with only a closure, repetition runs in a stack-neutral outer loop.
5Must the function be tail recursive?
Yes, it must first be rewritten into tail recursive form with an accumulator parameter.
6Does it work for mutual recursion?
Yes, several alternating functions can be run safely through the same trampoline.
7What overhead does it add?
Closure allocation per step and additional instanceof checks compared to a direct loop.
8When is a loop better?
For simple linear iteration over known, bounded data, a classic loop is almost always faster.
9Where does this typically show up?
Recursive parsing of nested data, deep category hierarchies, or algorithms on large datasets.
10Is the max depth fixed?
No, it depends on system, configuration and memory, typically a few thousand to some tens of thousands of calls.