Recursion vs Iteration — Recursion Crashed JSON Parser
RecursionError at 1000 nesting levels crashed payment API.
20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Recursion calls itself to solve smaller subproblems — elegant but costs a stack frame per call
- Iteration uses loops — lower overhead, predictable memory usage
- Python's default recursion limit is 1000 frames — exceeding it raises RecursionError
- Every recursive solution has an iterative equivalent via explicit stack management
- Memoization (@lru_cache) transforms exponential recursive algorithms into linear time
- Iteration is typically 3-10x faster than recursion due to avoided function call overhead
Imagine you need to find a book on a shelf. Recursion is like asking a friend, who asks another friend, who asks another friend — each person handles one book and passes the rest along. Eventually someone finds it, and the answer bubbles back up through the chain. Iteration is like checking each book yourself, one at a time, moving along the shelf. Both find the book. But recursion uses more phone calls (stack frames), and if the chain gets long enough, someone's phone dies — that's your stack overflow. Iteration uses more of your own time (loop cycles) but never runs out of phone battery.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Recursion and iteration both repeat logic, but they differ in how they manage state: recursion uses the call stack, iteration uses explicit loops. In production, the choice determines whether your JSON parser survives a 10,000-level nested payload or crashes with a stack overflow. Recursion is elegant for bounded-depth problems like balanced tree traversal; iteration is mandatory for any input that could be adversarial or unbounded. This article breaks down the mechanics, trade-offs, and conversion patterns so you can pick the right tool for your production code.
Why Recursion Crashed Your JSON Parser
Recursion and iteration are both mechanisms for repetition, but they differ fundamentally in how they manage state. Recursion relies on the call stack: each recursive call pushes a new frame, holding local variables and a return address. Iteration uses explicit loop constructs (for, while) and mutates a single set of variables in place. The core mechanic is that recursion expresses the problem in terms of smaller self-similar subproblems, while iteration sequences through a linear progression.
In practice, recursion buys you elegance at the cost of stack depth. Every call consumes a fixed amount of stack memory — typically 1–8 KB per frame in Java. A deeply nested JSON input (say, 10,000 levels) will blow the default stack size (~1 MB) and throw a StackOverflowError. Iteration, by contrast, uses heap-allocated data structures (e.g., an explicit stack or queue) that can grow to gigabytes before hitting memory limits. This makes iteration predictable and safe for unbounded input sizes.
Use recursion when the problem is naturally hierarchical (tree traversal, divide-and-conquer) and depth is bounded — e.g., balanced binary trees with log N depth. Use iteration when input depth is unbounded or when you need tight control over memory. In production parsers, the choice is not stylistic: it determines whether your service survives adversarial input or crashes silently under load.
Fibonacci — Recursion vs Iteration vs Memoization
Fibonacci is the canonical demonstration of recursion's cost because the pathology is so clear. The naive recursive version recomputes the same subproblems an exponential number of times — fib(5) calls fib(4) and fib(3), fib(4) calls fib(3) and fib(2), and fib(3) gets called again from fib(4)'s branch. The call tree doubles in size with each increment of n. By fib(40), you're making roughly a billion function calls to compute a number that a loop would find in 40 iterations.
Memoization fixes the exponential redundancy while keeping the recursive structure intact. @lru_cache stores the result of each unique (n,) argument tuple, so fib(3) is computed once and every subsequent call to fib(3) returns the cached result immediately. This collapses the O(2^n) call tree into a O(n) chain — you still recurse, but each unique subproblem executes exactly once.
Iteration beats both on space. The memoized version caches n results — O(n) space. The iterative version uses two variables that get updated in place — O(1) space. For computing fib(1_000_000), the iterative version uses negligible memory; the memoized recursive version would require thousands of cached entries and stack frames.
- Each plate = one function call's local variables, arguments, and return address
- Base case = the point where you stop adding plates and start removing them from the top
- Stack overflow = the stack grew taller than the OS allows — typically 1-8MB per thread
- Memoization = writing the result on each plate before putting it down so you never have to redo that calculation
- Iteration = using a single plate that you erase and rewrite in place — constant stack height regardless of n
Converting Recursive DFS to Iterative
The pattern for converting any recursive algorithm to iterative is mechanical once you understand it: the call stack that the language manages implicitly becomes an explicit data structure you manage yourself. Where the recursive version calls itself with a smaller problem, the iterative version pushes that smaller problem onto a list or deque. Where the recursive version returns and picks up where it left off, the iterative version pops from the list and continues the loop.
DFS is the cleanest example because the conversion is direct. The recursive DFS works by calling itself for each unvisited neighbour — the call stack naturally implements LIFO ordering, visiting the deepest unvisited node first. The iterative version replaces the call stack with a Python list used as a stack (append for push, pop for pop), producing identical traversal order.
The one subtlety that trips people up: push order. The recursive version processes neighbours left-to-right because it calls itself for the first neighbour, recurses all the way down that branch, then comes back and calls itself for the second neighbour. To match this order iteratively, you push neighbours in reverse — the last neighbour goes on the stack first, so the first neighbour gets popped and processed first.
This matters in interview settings and in production when the traversal order is semantically meaningful — for example, when processing rules in priority order or when the graph represents a dependency chain where the order of resolution matters.
Tail Recursion — Why Python Refuses to Optimize It
Tail recursion is a specific structural property of a recursive function: the recursive call is the very last operation executed before the function returns, with no pending computation waiting for the result. In a tail-recursive function, the current stack frame is no longer needed once the recursive call is made — all the information needed for the rest of the computation has been passed as arguments to the next call.
Languages like Scheme, Haskell, and Scala exploit this property through tail call optimization (TCO): the compiler recognizes that the current frame is no longer needed and reuses it for the next call instead of pushing a new one. The recursive call is effectively compiled into a jump instruction rather than a function call, making tail-recursive code consume constant stack space regardless of recursion depth. Tail recursion in these languages is genuinely equivalent to a while loop at the hardware level.
Python deliberately does not implement TCO. Guido van Rossum's reasoning has been documented publicly: stack traces are the primary debugging tool for Python developers, and TCO would make stack traces useless — instead of seeing the full chain of recursive calls, you would see a single frame. He also argued that Python is not a functional language and that the idiomatic solution for iteration is a loop, not compiler magic. This decision has never been reversed.
Java is in a similar position, though for a different reason. The JVM specification does not require TCO, and the HotSpot JVM does not implement it for general recursive calls. There is ongoing work in Project Loom and through invokedynamic to support functional-style TCO in some contexts, but as of 2026 you cannot rely on TCO in standard Java code.
The practical implication is stark: in Python and Java, tail-recursive code is exactly as expensive as non-tail-recursive code. Writing a function in tail-recursive style in Python provides zero performance benefit and no stack depth advantage. If you want iteration performance, write a loop.
When Recursion Wins — Tree Traversal and Divide and Conquer
Despite its costs, recursion is the right choice for problems whose structure is inherently recursive. Tree traversal is the clearest example: a tree is defined recursively (a node with left and right subtrees that are themselves trees), so a recursive traversal directly mirrors the definition. You can read the three-line recursive inorder traversal and immediately verify it is correct because the code and the definition are the same thing. The iterative equivalent requires managing an explicit stack, tracking multiple state variables, and reasoning about edge cases that simply do not exist in the recursive version.
Divide and conquer algorithms (merge sort, quicksort, binary search) have the same property. The recursive version of merge sort — split the array in half, sort each half, merge — is directly expressible in code. The iterative version requires managing multiple pointers and merge widths through nested loops, and is substantially harder to understand and verify.
The important constraint that makes recursion safe for these problems: the recursion depth is bounded by the structure of the data, not by user-controlled input. A balanced binary tree with one billion nodes has a depth of approximately 30. Even a perfectly degenerate tree (a linked list) has a depth equal to its node count — but if you know the tree is balanced (because you built it that way), depth 30 is well within any platform's stack limit.
The practical rule: use recursion when the problem structure is recursive AND you can bound the depth at design time. The moment depth is controlled by external input — user-uploaded files, API responses, user-constructed data structures — you must switch to iteration.
The Stack Blow-Up — Why Recursion Kills Your Production Server
Every recursive call consumes a stack frame. Each frame stores local variables, return addresses, and saved registers. Call a function 10,000 times recursively and you've allocated 10,000 frames. Iteration? One frame, one loop counter.
This isn't academic. Your JSON parser that recursively descends into nested objects? A deeply nested payload from a third-party API hits depth 1,001, and your JVM throws a StackOverflowError. No graceful degradation. No partial results. Just a 500 and a pager at 2 AM.
The standard JVM stack size is 1024 KB. Each Java frame costs about 40-80 bytes. Do the math: 1024 * 1024 / 60 ≈ 17,000 frames. That's your hard limit before the server eats itself. Iteration doesn't have a hard limit—only heap pressure from data structures.
So when your PM asks "why did the JSON parser crash?" the answer isn't "we need more memory." The answer is "our stack-based approach doesn't scale to production data."
Recursive JSON Parser Crashed Production API
parse_nested(). The errors were non-deterministic — some requests from the same payment provider succeeded and some failed — which initially made the team suspect a race condition rather than a depth issue. The pattern became clear only after correlating failed requests against payload size.- Never use recursion on untrusted input — you cannot control the depth, and the depth is often controlled by someone with different incentives than you
- sys.setrecursionlimit() only adjusts Python's internal frame counter — it does not increase the actual C stack size allocated by the OS; raising it beyond a few thousand reliably causes segfaults
- Always validate and reject input that exceeds your recursion budget before beginning recursive processing — fail fast with a 400, not slow with a 500
- Iterative parsing with an explicit stack is mandatory for production systems processing external or user-controlled data; the performance difference is negligible and the safety difference is absolute
sys.setrecursionlimit() as the first fix — this delays the segfault rather than preventing it. Add a depth guard at the entry point: if depth > MAX_DEPTH: raise ValueError(f'Input exceeds maximum depth {MAX_DEPTH}'). Then convert the recursive path to iterative using an explicit stack.set() at the top. Also check for accidental global state modification inside the recursive function.python -c "import sys; print(sys.getrecursionlimit())"python -c "import sys; print(sys.getrecursionlimit()); sys.setrecursionlimit(2000)"Key takeaways
sys.setrecursionlimit() moves the crash threshold but causes a segfault when the C stack is exhausted.Practice These on LeetCode
Interview Questions on This Topic
What is Python's default recursion limit, how do you change it, and what are the risks of changing it?
sys.getrecursionlimit(). You can change it with sys.setrecursionlimit(n). The risk: this limit only adjusts Python's internal frame counter — it does not increase the actual C stack size that the OS allocates for the thread. The C stack is typically 1-8MB depending on the OS and how the thread was created. Setting the limit to 100000 and recursing that deep will cause a segfault (unhandled C stack overflow) rather than a catchable RecursionError. The process exits without a useful stack trace. The correct fix for deep recursion is always to convert to iterative — not to raise the limit.Frequently Asked Questions
20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.
That's Recursion. Mark it forged?
5 min read · try the examples if you haven't