Python
RecursionError: maximum recursion depth exceeded
Written and reviewed by Sahil Srivastav
Traceback (most recent call last):
File "app/tree.py", line 18, in walk
return walk(node.child)
RecursionError: maximum recursion depth exceededWhat this error actually means
CPython stops recursive calls before the native thread stack is exhausted. Each Python-to-Python call consumes interpreter frames, and the recursion guard raises this exception when the configured limit is reached. The default is deliberately conservative because a deeper call chain can crash the process rather than produce a catchable exception.
The limit is a symptom boundary, not a complexity budget. A missing base case and a valid traversal of a deeply skewed tree both hit it, but raising the limit only helps the second case and can turn the first into a segmentation fault. Recursive code that revisits the same object through a parent pointer is especially deceptive: the input looks finite when printed, but the reachable graph is cyclic.
The traceback’s repeated frames tell you which operation failed, not whether the data or the algorithm is wrong. Count depth, inspect identity edges, and decide whether the operation should be iterative. Most production fixes replace recursion with an explicit stack and retain a visited set for graph-shaped data.
Causes, most common first
- 1A missing or unreachable base case. The recursive call does not move toward termination, often because an empty list, null child, or already-processed state is handled after the recursive call.
- 2A cycle in data treated as a tree. Parent links, ORM back references, or graph edges lead back to an ancestor. Depth increases forever even though the number of objects is finite.
- 3Legitimate depth exceeding the interpreter guard. A generated expression, nested directory, or degenerate binary tree can be thousands of levels deep. The algorithm terminates, but recursion is the wrong storage for that depth.
- 4Recursive representation or equality. A custom `__repr__`, `__eq__`, or serializer walks a self-referential object and hides the original application frame behind formatting.
When you see it
- The traceback repeats the same function dozens of times
- Only one deeply nested tenant or document triggers the crash
- JSON or repr output itself raises a recursion error on cyclic objects
- Increasing the limit makes the process slower or unstable
- A worker dies while ordinary inputs continue to pass
How to diagnose it
Step 1
Print the limit and count frames
Confirm the environment rather than guessing. The limit applies per interpreter and may have been changed by a test or embedding application.
python - <<'PY'
import sys, traceback
print(sys.getrecursionlimit())
try: walk(root)
except RecursionError:
traceback.print_exc(limit=12)
PYStep 2
Detect identity cycles
For graph-like input, track object identity rather than equality. Equality may itself recurse, while `id` is constant for the object’s lifetime.
python - <<'PY'
def visit(node, seen=None):
seen = set() if seen is None else seen
if id(node) in seen: raise ValueError('cycle')
seen.add(id(node))
for child in node.children: visit(child, seen)
seen.remove(id(node))
PYStep 3
Capture the failing input
Log a bounded identifier and depth, not the whole object. A recursive repr can trigger the same exception while you are trying to diagnose it.
The fix
Add a base case before the recursive call and make each call consume input. Test empty, one-element, and cyclic cases explicitly.
For graphs, maintain a visited set keyed by stable node identity or database key. Remove nodes from the active path only when you need to distinguish a cycle from a previously completed node.
Replace deep recursion with an explicit list or deque. This moves depth from the C stack to heap memory, where it can be bounded and monitored.
Raise `sys.setrecursionlimit` only for a measured, finite depth that you own. Test the maximum input in the same process model; the setting is not a substitute for cycle detection.
If serialization is the failing operation, use a serializer with reference handling or project objects to IDs and fields rather than traversing ORM back references.
def walk(root):
stack = [root]
seen = set()
while stack:
node = stack.pop()
key = id(node)
if key in seen:
continue
seen.add(key)
yield node
stack.extend(reversed(node.children))How to stop it coming back
- Fuzz nested and cyclic structures, not only balanced trees
- Set an explicit maximum document depth at input validation
- Keep recursion limits unchanged in production unless a design review records why
- Emit depth histograms for user-controlled nested data
- Treat recursive repr and serializers as production code with cycle tests
FAQ
Should I call sys.setrecursionlimit(100000)?
No. It can postpone a valid-depth failure, but for an accidental cycle it permits more frames before a native stack crash. Prove the maximum depth and prefer iteration.
Is this always caused by infinite recursion?
No. A terminating traversal of a very deep structure can exceed the guard. The repeated frames and input depth distinguish it from a call that never makes progress.
Why does printing the object fail too?
The printer may follow the same cycle through repr or a custom serializer. Print IDs, types, and bounded fields instead of the complete object.
Related
Other errors engineers hit next to this one
- Exactly-once claim fails at an external side effect
- Dead-letter queue growing without an alert
- Idempotency key reused with a different request body
- Read-after-write returned stale data from a replica
- Replication lag: replica served stale data after a write
- FATAL ERROR: Reached heap limit Allocation failed
- Unhandled promise rejection crashes the process
- ECONNRESET: socket hang up on a reused connection