Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA)  Module 4 – Recursion and backtracking

Recursion limits in every runtime, and the fix

Measure how deep recursion goes in CPython, Pyodide, Node.js, QuickJS, Java, C++ and Go, and rewrite deep recursion as a loop with an explicit stack.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 9 of 30 minutes free
Free: Why a runtime limits recursion · The limits, measured · The fix: an explicit stack · Exercise. With a pass: 3 in-depth parts and 3 interview answers, about 21 minutes.

What you will learn

  • Predict when a recursive solution will overflow in each runtime
  • Convert recursion to iteration with an explicit stack
  • Choose safe recursion depths for code that runs in the browser
  • Configure a larger stack when a rewrite is not possible, and name what can still fail

Before you start

What you’ll learn · Before you start

What you will learn

  • Predict when a recursive solution will overflow in each runtime
  • Convert recursion to iteration with an explicit stack
  • Choose safe recursion depths for code that runs in the browser
  • Configure a larger stack when a rewrite is not possible, and name what can still fail

Before you start

On this page

Every runtime caps how deep recursion can go, because each call that has not returned keeps a frame, and frames live on a stack of fixed size. CPython counts its frames and stops at its recursion limit, 1,000 by default, with RecursionError. Node.js and Java run until the thread’s stack is nearly full and then throw an error; a C++ program writes past the end of its stack and crashes. Go grows each goroutine’s stack until it reaches 1 GB. None of them turns a tail call into a loop for you. So when the depth can reach about 10,000, as it does with a long linked list, a path-shaped graph or a deeply nested input, the fix is to write the loop yourself, with an explicit stack. This lesson measures the recursion limits in every runtime this track uses, then builds the fix in Python.

Why a runtime limits recursion

A frame holds a call’s arguments, its local variables and the point where it carries on, and it stays on the stack until the call returns. A thread’s stack is memory reserved when the thread starts: 8,176 KiB for a program’s first thread on the Mac that recorded these runs (ulimit -s shows it), and 1 MB by default for a Windows program built with Microsoft’s linker. If frames fill it, the next call writes past the end, and POSIX says the thread then receives SIGSEGV: a segmentation fault, with no word about recursion.

Managed runtimes check first. CPython raises RecursionError when its count of Python frames reaches sys.getrecursionlimit(), a limit its documentation says exists to keep infinite recursion from overflowing the C stack. Node.js and Java watch the stack that is left and throw RangeError or StackOverflowError. Go grows each goroutine’s stack by copying it to a larger one, up to runtime/debug.SetMaxStack, 1 GB on 64-bit systems.

The limits, measured

The probe is one function that calls itself, one level deeper each time, until something stops it. In Python:

How deep Python lets a probe go Python · probe/probe.py
import sys


def deepest():
    """How many nested calls of a one-argument function start before RecursionError."""

    def down(n):
        try:
            return down(n + 1)
        except RecursionError:  # the call to down(n + 1) could not start
            return n

    return down(1)


print(f"limit {sys.getrecursionlimit():>7,}: deepest call {deepest():>7,}")
for limit in (10_000, 100_000):
    sys.setrecursionlimit(limit)
    print(f"limit {limit:>7,}: deepest call {deepest():>7,}")

Output

limit   1,000: deepest call     998
limit  10,000: deepest call   9,998
limit 100,000: deepest call  99,998

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 probe.py

Version note

The Run button runs Pyodide, CPython compiled to WebAssembly. Its recorded run reached 993 calls instead of 998, because the runner’s own code holds five frames before the program starts.

In your browser, Pyodide 314.0.7 (CPython 3.14.2) prints:

limit   1,000: deepest call     993
limit  10,000: deepest call   9,993
limit 100,000: deepest call  99,993

The limit is a counter, not a measure of memory: 998 calls fit under the default of 1,000, since the module and the function that starts the probe hold two frames, and 99,998 once the limit is 100,000. Written in each language, the same probe gives the recursion limits in every runtime of this track:

Runtime Depth reached How it ends
CPython 3.14.8 998, or 99,998 with the limit at 100,000 RecursionError, at its frame count
Pyodide 314.0.7, the Python runner in the browser 993 RecursionError, or the runner stops when code passes through C
Node.js 24.21.0 about 10,400 RangeError, at V8’s 984 KB share of the stack
QuickJS 0.32.0, the JavaScript runner in the browser 1,600 ran, 1,800 did not the runner stops
Java 25, main thread 17,639, or more once compiled StackOverflowError, on a 2 MiB stack
C++ (Apple clang 21, -O0) 250,000 ran, 270,000 crashed a segmentation fault, on an 8 MiB stack
Go 1.27.1 10 million ran a fatal error, once a stack passes 1 GB

The browser rows were measured with the runners themselves under Node.js 24.21.0; a browser gives its worker a stack of its own choosing, so treat them as upper bounds.

The fix: an explicit stack

Deep recursion usually comes from a chain: a linked list, a path-shaped graph, a list nested inside itself. Here is a search of a path of 100,000 vertices, both ways:

A recursive search and the same search with a list as its stack Python · search/dfs_path.py
def path_graph(n):
    """n vertices in a line, each joined to the next: 0-1, 1-2, ..., (n-2)-(n-1)."""
    adj = [[] for _ in range(n)]
    for v in range(n - 1):
        adj[v].append(v + 1)
        adj[v + 1].append(v)
    return adj


def reach_recursive(adj, start):
    seen = set()

    def visit(v):
        seen.add(v)
        for w in adj[v]:
            if w not in seen:
                visit(w)  # one frame per vertex on the current path

    visit(start)
    return len(seen)


def reach_with_stack(adj, start):
    seen = {start}
    stack = [start]  # vertices found but not expanded yet
    tallest = 1
    while stack:
        v = stack.pop()
        for w in adj[v]:
            if w not in seen:
                seen.add(w)
                stack.append(w)
        tallest = max(tallest, len(stack))
    return len(seen), tallest


for n in (500, 100_000):
    adj = path_graph(n)
    try:
        rec = f"{reach_recursive(adj, 0):,} reached"
    except RecursionError:
        rec = "RecursionError"
    found, tallest = reach_with_stack(adj, 0)
    print(f"path of {n:,} vertices: recursive {rec}; explicit stack {found:,} reached, never more than {tallest} waiting")

Output

path of 500 vertices: recursive 500 reached; explicit stack 500 reached, never more than 1 waiting
path of 100,000 vertices: recursive RecursionError; explicit stack 100,000 reached, never more than 1 waiting

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dfs_path.py

Recursion keeps 100,000 frames on the call stack and hits the limit; a loop keeps a small list on the heap instead.Recursion: visit(v) calls visit(w)Loop: an explicit stackthe call stackvisit(0), visit(1), …, visit(99999)100,000 frames at onceCPython stops at 1,000:RecursionErrorstack = [ … ], a Python listholds vertices found, not expandedat most 1 entry on this patha list lives on the heap:no frame limit appliessame vertices,same number of steps

The same search with frames and with an explicit stack

Text description of the diagram

The diagram compares two ways to search a path of 100,000 vertices, from top to bottom.

  • With recursion, visit(v) calls visit(w) for the next vertex, so the call stack holds visit(0), visit(1) and so on up to visit(99999): 100,000 frames at the same time. CPython stops at its limit of 1,000 frames with RecursionError.
  • With a loop, a Python list serves as an explicit stack. It holds only the vertices that have been found but not expanded yet, which on a path is never more than one. The list lives on the heap, where no frame limit applies.

Both versions visit the same vertices with the same number of steps.

The recursive search keeps a frame for every vertex on the current path, because each call is still inside its loop over neighbours when it makes the next call: 100,000 frames, far past the limit. The loop keeps only the vertices found but not yet expanded, in an ordinary list on the heap, where no frame limit applies; on a path that list never holds more than one vertex. Both do O(V + E) work for V vertices and E edges. The fix moves the pending work; it adds none.

The loop expands vertices in another order than the recursion, which is fine for reachability. When the work after each call matters, as for subtree sizes, each stack entry holds a vertex and the index of its next child, exactly what a frame remembers, and the recursive order is kept step for step.

Exercise

Exercise · Easy · Python

Sum a deeply nested list without recursion

nested_sum(nested) adds up every integer in a list that may hold integers and other lists, to any depth: nested_sum([1, [2, [3]], 4]) is 10. The starter code does it with recursion, one call per level of nesting, so a list nested 3,000 levels deep makes it fail with RecursionError long before it finishes.

Rewrite nested_sum without recursion. Keep a list as an explicit stack of the lists still to be read: start with the outer list on it, and until the stack is empty, pop a list and go through its items, adding integers to the total and pushing inner lists onto the stack. The order in which the integers are added does not change the sum.

The sample tests check small lists, empty lists, lists of lists side by side, a list nested 3,000 levels deep, which only a version without recursion survives, and that the input is left as it was. Do not change the recursion limit: the point is a function that does not depend on it. (The test stops at 3,000 levels because much deeper nesting can stop the browser runner itself when Python frees the list, whatever nested_sum does.)

Starter code · nested_sum.py

def nested_sum(nested):
    """The sum of every integer in a list nested to any depth."""
    # One call per level of nesting: fails at a depth of about 1,000. Rewrite it with an explicit stack.
    total = 0
    for item in nested:
        if isinstance(item, list):
            total += nested_sum(item)
        else:
            total += item
    return total
The sample tests · test_nested_sum.py
import copy

from nested_sum import nested_sum


def deep_list(levels):
    """[1, [2, [3, ... [levels, []] ... ]]], built with a loop."""
    nested = []
    for value in range(levels, 0, -1):
        nested = [value, nested]
    return nested


def test_small_lists():
    """flat and nested lists of a few integers"""
    assert nested_sum([4, 5, 6]) == 15
    assert nested_sum([1, [2, [3]], 4]) == 10


def test_empty_lists():
    """empty lists add nothing, at any depth"""
    assert nested_sum([]) == 0
    assert nested_sum([[], [[]], [[], [9]]]) == 9


def test_lists_side_by_side():
    """many inner lists next to each other, and negative numbers"""
    assert nested_sum([[1, 2], [3, [4, -10]], [[5], 6]]) == 11


def test_three_thousand_levels():
    """a list nested 3,000 levels deep, three times the default recursion limit"""
    assert nested_sum(deep_list(3_000)) == 3_000 * 3_001 // 2


def test_input_left_alone():
    """the list it is given is not changed"""
    data = [1, [2, [3, []]], [4]]
    before = copy.deepcopy(data)
    assert nested_sum(data) == 10
    assert data == before
A hint

Start with total = 0 and stack = [nested]. In a while stack: loop, take current = stack.pop() and loop over its items: add an integer to total, and stack.append(item) for a list, to read later. Return total when the stack is empty.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 A recursive function makes one call per node to walk a linked list of 50,000 nodes, in CPython with the default settings. What happens?

    Choose one answer.

    Show the answer to question 1

    Answer: It raises RecursionError after about 1,000 calls

    CPython counts the frames that are waiting and raises RecursionError when the count reaches the recursion limit, 1,000 by default. Each node adds a frame that waits for the rest of the list, so 50,000 nodes go far past it.

  2. Question 2 of 4 Which change lets a search of a very long path run in every runtime of this lesson?

    Choose one answer.

    Show the answer to question 2

    Answer: Rewriting it as a loop with an explicit stack in a list

    A list on the heap has no frame limit in any runtime. Raising the limit is a Python setting that does not change the stack's size, no runtime here promises to turn tail calls into loops, and an optimiser may or may not remove the recursion.

  3. Question 3 of 4 On the path of 100,000 vertices, at most how many vertices did the explicit-stack search keep waiting in its list?

    Type a number.

    Show the answer to question 3

    Answer: 1

    On a path, expanding a vertex finds exactly one new vertex, the next one, so the list holds one vertex at a time, as the recorded run says. The recursive version keeps 100,000 frames instead, one for each vertex on the path.

  4. Question 4 of 4 An unoptimised C++ build recurses 270,000 levels deep on an 8 MiB stack. What does its user see?

    Choose one answer.

    Show the answer to question 4

    Answer: The program stops with a segmentation fault and no message about recursion

    C++ has no depth counter of its own. The frames write past the end of the stack, and POSIX says the thread then receives SIGSEGV, which ends the program. Python, Java and JavaScript turn the same situation into an exception.

The same probe in five languages, measured Pro: opens with a Pro, Premium or Ultimate pass

The probe is one small function that calls itself until something stops it. Here it runs in JavaScript, Java, C++ and Go with each runtime’s own stack settings, and in CPython through C code, where a second limit applies.

In this part:

  • The probe in JavaScript, Java, C++ and Go
  • Measured: each runtime, its stack settings and how it fails
  • Python’s second limit: recursion that passes through C code

3 runnable examples (C++, Go, Java, JavaScript, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Rewriting any recursion with an explicit stack, checked Pro: opens with a Pro, Premium or Ultimate pass

A list of vertices is enough when nothing happens after a call returns. When work does happen after the call, the stack must hold frames; this part builds them, shows why they follow the recursion step for step, and checks them on thousands of trees.

In this part:

  • Frames on the heap: a vertex and the index of its next child
  • Why the frames follow the recursion step for step: the invariant
  • Measured on a path of 100,000 vertices
  • Questions to ask before rewriting

1 diagram · 1 runnable example (Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Safe depths for browser-run code, and raising a limit Pro: opens with a Pro, Premium or Ultimate pass

Some code must stay recursive, and some runs where the stack is smaller than on a laptop. This part sets depth budgets for the browser runners and shows how to raise the limit in each runtime, with what each change can still break.

In this part:

  • A depth budget for the browser runners, measured
  • Raising the limit in each runtime, and what can still fail
  • Key takeaways of these parts

3 runnable examples (JavaScript, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What does sys.setrecursionlimit() change in CPython, and what does it leave unchanged?

    It changes how many Python frames CPython lets pile up before it raises RecursionError: 1,000 by default, so about 1,000 nested calls of a Python function. It does not change any memory. The thread's stack keeps the size it got when the thread started; threading.stack_size() sets it only for threads started later. Since Python 3.14, recursion that passes through C code, such as sum() over a generator that calls the function again, is checked against the C stack that is left, and the documentation says that check ignores the recursion limit, so it can still fail with "RecursionError: Stack overflow" far below a high limit. The documentation also warns that a limit set too high can crash Python. A higher limit does not make recursion faster, and it does not turn tail calls into loops: CPython does not optimise them.

  2. Mid to senior

    A recursive depth-first search over a dependency graph of 200,000 packages passes its tests but fails in production with RecursionError. You may not change interpreter settings. How do you rewrite it, and how do you show that the new version visits the packages in exactly the same order?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A teammate fixes a RecursionError with sys.setrecursionlimit(10**6). It works on their laptop, but the same job fails with "RecursionError: Stack overflow" on one input and stops the browser-based runner on another. Explain both, and say when raising the limit is a reasonable fix.

    Model answer, Pro. First step and follow-ups

  4. Fresher to mid

    Your recursive C++ depth-first search passes on your machine, but an online judge reports a segmentation fault when the graph is a single path of 10^6 vertices. Explain why, estimate the stack it needs, and give two fixes.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Every unfinished call keeps a frame on a fixed-size stack, so every runtime limits depth; only CPython’s limit is a counter.
  • One probe: CPython about 1,000 calls, Node.js about 10,400; unoptimised C++ crashed between 250,000 and 270,000.
  • The fix that works everywhere is an explicit stack: the pending work in a list on the heap.
  • The browser runners are the tightest place: keep their recursion shallow, or write the loop.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.