The three classic memory crashes
With the layout in hand, the famous crashes stop being mysterious:
- Stack overflow. The stack has a fixed and fairly small size, a few megabytes. If function calls keep nesting without returning, frames pile up until the stack runs out of room, and the usual cause is runaway recursion.
- Out of memory, often written OOM. The heap can grow, but not forever. If a process keeps allocating, the OS eventually refuses, or a system OOM-killer terminates the process outright.
- Segmentation fault, usually shortened to segfault. The process touches an address it does not own, such as by following a bad pointer in C. The hardware traps it and the OS kills the process, which is process isolation from lesson 3-1 doing its job.
| Crash | Region that ran out or was violated |
|---|---|
| stack overflow | the stack |
| out of memory | the heap |
| segfault | an address outside the process |
Python protects you from segfaults and converts stack overflow into a catchable RecursionError, but the machine underneath is the same, which is why the same three failures show up in every language.
Recursion that returns
Each call pushes a frame and each return pops one, so even 500 levels deep is fine.
def countdown(n): if n == 0: return "liftoff" return countdown(n - 1) print(countdown(5)) print(countdown(500))
Output
liftoff liftoff
countdown(500) briefly stacks 501 frames, then they all pop as the returns unwind. The peak depth is what costs memory, not the total number of calls.
The base case is doing all the safety work here. if n == 0 is the only branch that returns without calling again, and it is reachable because every recursive call moves n one step closer to it.
Recursion that never returns
forever has no base case, so frames pile up until Python's stack limit stops it. Catching RecursionError shows the crash without ending the program.
import sys print(sys.getrecursionlimit() > 0) def forever(n): return forever(n + 1) try: forever(1) except RecursionError: print("crashed: the call stack overflowed")
Output
True crashed: the call stack overflowed
sys.getrecursionlimit() is usually 1000, and Python counts frames against that limit rather than waiting for the real stack to run out.
That limit is a courtesy. In C the same mistake is a genuine stack overflow that the hardware catches, and there is no exception to handle, only a dead process, which is why the guard exists in the first place.
The base case that stops the recursion
The same countdown with the terminating branch in place.
def countdown(n): if n == 0: return "liftoff" return countdown(n - 1) print(countdown(3))
Output
liftoff
The base case is the condition where the function returns without calling itself again, and it has to be placed before the recursive call so it can win.
Two things must both hold for recursion to be safe. There must be a base case, and every recursive call must move toward it, which countdown(n - 1) does. Writing countdown(n + 1) with the same base case would be just as broken as having no base case at all.
Reading the crash like an engineer
These are the symptoms as they appear in the wild, with the first thing to check for each:
| Symptom | Likely cause | First thing to check |
|---|---|---|
RecursionError or stack overflow | recursion with no reachable base case | whether the recursive call always moves toward the base case |
process killed, MemoryError, machine crawls then a process dies | unbounded growth in a list or cache | which collection grows on every request or loop iteration |
Segmentation fault (core dumped) | native code touched memory it does not own | which C extension or native library was involved |
The timing is a strong clue on its own. A stack overflow dies in milliseconds, a segfault dies at the exact instant of the bad access, and an out-of-memory death arrives hours or days in.
The pattern to internalize is that crashes are the OS and the hardware enforcing the rules from unit 1 and lesson 3-1, not random bad luck. Each one names a specific rule that was broken.
Diagnosing a service that dies after days
A web service that slowly uses more RAM over days until the OS kills it, and that a restart fixes for a while, is a memory leak filling the heap until out-of-memory.
Slow growth over time plus death by the OS is the signature of a heap leak. Something keeps references to objects, often a growing cache or list, so they can never be freed.
| Family | Time to failure |
|---|---|
| stack overflow | milliseconds |
| segfault | instant, at the bad access |
| heap leak into OOM | hours or days |
A restart appears to fix it because a fresh process starts with an empty heap, which is why scheduled restarts are a common stopgap and never a fix. The real fix is finding the collection that only ever grows and giving it a bound.
Naming the runaway-recursion crash
A function that calls itself with a base case that can never be reached piles up frames until the program dies. That is a stack overflow.
Every unfinished call keeps a frame on the stack, and the stack region has a fixed size, so runaway recursion overflows it. Python surfaces this as RecursionError before the real stack dies.
The name states which memory region ran out of space, which is why it is the useful name rather than something generic such as "recursion error".
A very popular programming question-and-answer site is named after this crash, which is a decent hint at how often it happens.