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.
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:
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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:
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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 assum()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. -
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?
-
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.
-
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.
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
- sys.setrecursionlimit() and sys.getrecursionlimit() (Python Software Foundation)
- Recursion control: Py_EnterRecursiveCall() (Python/C API reference) (Python Software Foundation)
- threading.stack_size() (Python Software Foundation)
- What's New In Python 3.14: tail call optimization of Python functions is not implemented (Python Software Foundation)
- Class Thread, constructor with stackSize (Java SE 25) (Oracle)
- The java command (JDK 25): -Xss (Oracle)
- Package runtime/debug: SetMaxStack (The Go Authors)
- getrlimit, setrlimit: RLIMIT_STACK (POSIX.1-2024) (The Open Group and IEEE)
- /STACK (Stack allocations), MSVC linker reference (Microsoft)
- ECMAScript 2026 Language Specification: 15.10 Tail Position Calls (Ecma International)
- QuickJS documentation: language support (tail calls not supported) (Fabrice Bellard (QuickJS))
- quickjs-emscripten: runtime limits (setMaxStackSize) (quickjs-emscripten project)
- Pyodide documentation (Pyodide project)
- NIST Dictionary of Algorithms and Data Structures: tail recursion (National Institute of Standards and Technology (NIST))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress