skip to content

Memory and Stack Exhaustion

Running out of headroom rather than crashing on a bug: MemoryError against a kernel OOM kill, RecursionError from runaway recursion, and how to tell which one ended the process.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

3

What causes a Python RecursionError, and what do sys.getrecursionlimit and sys.setrecursionlimit control?

level: juniorimportance: must knowfreq 58%

answer

  1. A guard rail, not a measurement
  2. Counts frames, not bytes
  3. Default ceiling is one thousand
  4. Read the traceback for the repeating cycle
  5. Ceiling is process-wide, depth per thread

basics

~20 s

CPython counts the Python frames stacked on the current thread and raises RecursionError once that count passes the ceiling sys.getrecursionlimit() reports, which is 1000 on a fresh interpreter. sys.setrecursionlimit() moves the ceiling; it does not enlarge the thread's real stack.

solid answer

~50 s

`RecursionError` is a `RuntimeError` subclass raised when the number of nested Python frames on a thread exceeds the interpreter's recursion limit; `sys.getrecursionlimit()` returns **1000** on a fresh interpreter. That number counts **frames, not bytes** — it is a guard rail so that runaway recursion produces a catchable exception instead of a process-level crash. `sys.setrecursionlimit(n)` moves the ceiling **process-wide** and takes effect immediately, though each thread counts its own depth against it. In practice a `RecursionError` almost always means the base case is never reached or the data has a cycle — a plain recursive walk over a dependency graph of 17 services with one back edge and no visited set, for example. The fix is to bound the traversal or rewrite it iteratively with an explicit stack; raising the limit is the last resort, not the first.

code

python · 12 lines
python
import sys

print(sys.getrecursionlimit())  # 1000 on a fresh interpreter

def depth(n=0):
    return depth(n + 1)

try:
    depth()
except RecursionError as exc:
    print(type(exc).__name__, "-", exc)
    print("still a RuntimeError:", isinstance(exc, RuntimeError))

go deeper

for a junior

Be ready to say the default is 1000, that the exception is RecursionError, and that it counts nested calls rather than memory. Showing a missing base case and fixing it is enough at this level.

for a middle

Explain the mechanics: frames counted per thread against one process-wide ceiling, RecursionError as a RuntimeError subclass, and the mechanical rewrite from recursion to an explicit stack with a visited set.

for a senior

Demonstrate judgment about when raising the limit is legitimate — a bounded parser depth — versus when it hides a cycle, and be able to read a recursive traceback back to the offending edge in the data.

for a principal

Own the policy angle: a process-wide setting that any library can change is shared mutable state, so decide where recursion depth is allowed in your codebase at all and how deeply nested untrusted input is bounded before it reaches Python.

## What the limit actually counts Every Python function call pushes a frame onto the calling thread's frame stack. CPython keeps a running count of how deep that stack is and compares it against a single process-wide ceiling before pushing another one. When the next push would exceed the ceiling, the interpreter raises `RecursionError` with the message *maximum recursion depth exceeded*. `sys.getrecursionlimit()` reads the ceiling — **1000** on a fresh interpreter — and `sys.setrecursionlimit(n)` writes it. The most important thing to understand about that number is what it is *not*. It is a **count of frames**, not a measurement of memory. The interpreter is not asking "how many bytes of stack do I have left?" when it raises; it is asking "have I pushed a thousand frames?". Two different recursive functions — one with two locals, one with forty — hit the same ceiling at the same depth even though they consume very different amounts of space. ## Why the ceiling exists Without it, an unterminated recursion would keep pushing until the operating system's stack for that thread was exhausted, and the process would die from the outside with no traceback. The recursion limit converts that into an ordinary Python exception: it unwinds normally, `finally` blocks run, and calling code can catch it. Because `RecursionError` subclasses `RuntimeError`, a broad `except RuntimeError` catches it too — which is occasionally a nasty surprise in code that meant to catch something else. ## What it means when you actually see one Almost always: a missing or unreachable base case, or a **cycle in the data**. Consider walking a dependency graph of 17 services with a straightforward recursive `resolve(service)` and no set of already-visited nodes. One back edge — service A depends on B, B on C, C back on A — and the walk never terminates. The recursion limit does not diagnose that; the traceback does. Read it for the repeating block of frames: the cycle of function names that repeats is the loop. A much rarer second cause is legitimately deep data: a parser recursing once per nesting level over a deeply nested document, or a linked structure thousands of nodes long. That is the only case where the ceiling itself is the problem. ## Fixes, in the order to try them 1. **Bound the recursion.** Add the visited set, fix the base case, reject cyclic input. 2. **Rewrite iteratively.** Any recursion can be expressed with an explicit stack (a `list` used as a stack, or `collections.deque`), which moves the depth onto the heap where it is limited only by memory. For graph and tree walks this is usually a small, mechanical change and it removes the ceiling from the picture entirely. 3. **Only then raise the limit**, and only with a specific known depth bound in mind. `sys.setrecursionlimit(10_000)` for a parser whose input you have bounded is defensible. `sys.setrecursionlimit(10 ** 9)` to make a symptom go away is not — it turns a clean exception into an unbounded run. ## Depth is per thread; the ceiling is per process Each thread has its own frame stack and its own depth counter, but `sys.setrecursionlimit()` sets **one value shared by the whole process**. There is no per-thread recursion limit in Python. What *is* per thread is the amount of real stack the operating system gave that thread, which is where `threading.stack_size()` comes in for threads you create yourself. ## The version story, because the old advice has aged Older material warns that raising the recursion limit will segfault the interpreter. That warning was literally true on CPython 3.11 and earlier, where a Python-level call also consumed a C stack frame. Two changes softened it. **3.11** inlined Python-to-Python calls, so pure-Python recursion no longer costs a C stack frame per call. **3.12** made the recursion limit apply only to Python code, with built-in and C-level recursion protected by a separate mechanism that prevents a virtual-machine crash. On **3.14**, setting the limit to two million and recursing without a base case still surfaces a clean `RecursionError` rather than a crash — including for recursion that crosses C code, such as taking `repr()` of a deeply nested list. The practical guidance is unchanged (do not raise it to paper over a bug), but the mechanism you cite should be the current one. ## What it is not `RecursionError` is not `MemoryError` and has nothing to do with heap exhaustion; you can hit it in a process using a few megabytes. And it is not the kernel killing your process — if the interpreter raised an exception you can catch, the interpreter was still alive to raise it.

  • Why is calling sys.setrecursionlimit() with a very large number a poor fix for a RecursionError?
    Because the exception is nearly always reporting a real defect — an unreachable base case or a cycle in the data — and raising the ceiling only lets the defect run longer before it fails, now consuming far more memory and CPU on the way. The limit is also process-wide, so one module's convenience setting silently changes behaviour for every other thread and library in the process.
  • Is the recursion limit per thread or per process?
    The limit is a single process-wide value; `sys.setrecursionlimit()` changes it for every thread at once, and there is no per-thread override. What is per thread is the *depth counter* — each thread counts its own frames — and the actual operating-system stack that thread was given, which for threads you create yourself is influenced by `threading.stack_size()`.
  • How would you turn a recursive tree walk into one that cannot raise RecursionError?
    Move the depth onto the heap: keep an explicit stack of nodes still to visit — a `list` used as a stack, or a `collections.deque` — pop from it in a `while` loop, and push each node's children instead of calling yourself. Depth is then bounded by available memory rather than by a frame count, and a visited set on the same loop also protects you from cycles.

The limit is a turnstile counter at the door, not a fire marshal measuring the room: it stops you at a thousand people whether they are toddlers or sumo wrestlers.

saying these in an interview costs you the question

  • Saying the recursion limit measures remaining stack bytes
  • Treating sys.setrecursionlimit as the standard fix
  • Confusing RecursionError with MemoryError or an out-of-memory kill
  • Believing each thread can carry its own recursion limit
  • Claiming the default limit is unlimited or platform-dependent
  • Not reading the traceback for the repeating cycle of frames

context

open as a page

Exit status 137 and no traceback: how do you tell a kernel OOM kill of a Python process from a MemoryError?

level: seniorimportance: should knowfreq 47%

basics

~10 s

A MemoryError is an ordinary Python exception, so the interpreter is alive to print a traceback and run handlers. A kernel OOM kill is SIGKILL from outside: no traceback, no cleanup, exit status 137.

open as a page

What does threading.stack_size do, and how does it let deeper recursion run in a Python worker thread?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

threading.stack_size(size) sets the stack size requested for threads started after the call, and returns the previous value. Pair it with a raised sys.setrecursionlimit() to run deep recursion in that worker; the main thread's stack is fixed by the OS.

open as a page