Go Recursion Limit and Stack Overflow: The Interview Guide

- Go's goroutine stack starts at ~2 KB and grows automatically up to 1 GB, making recursion depth a non-issue for typical interview inputs.
- Fatal stack overflow in Go cannot be caught with
recover()— the program terminates immediately with no error-handling path. - No tail call optimization in Go: every recursive call allocates a new frame, so tail-recursive code still uses O(n) stack space.
runtime/debug.SetMaxStacklets you adjust the per-goroutine ceiling — knowing it exists signals runtime depth rather than needing to use it.- Go's 1 GB default dwarfs every other interview language: Python crashes at ~1,000 frames, Java at ~8,000–12,000, C++ at ~40,000.
- When asked about large inputs, give the three-part answer: Go handles it fine, recursive code still uses O(n) space, and iterative is the right choice for unbounded production depth.
Python gives up at 1,000 recursive calls. No warning, just RecursionError: maximum recursion depth exceeded and a sulky exit. Go, on the other hand, will cheerfully run your recursive function until it has consumed 1 GB of stack memory before it finally dies in a way you absolutely cannot catch.
That difference matters when you are live in an interview and your recursive DFS hits a sorted array fed into naive quicksort, or a "binary tree" that is secretly a 50,000-node linked list in a trench coat.
Go Stacks Don't Work Like Other Languages
Every goroutine gets its own stack. Not a fixed slab of memory carved out at startup like Java does. It starts at around 2 KB and grows automatically by copying the entire stack to a larger allocation whenever it runs out of room. This is the contiguous stack model, and it has been the default since Go 1.4.
When a function call would overflow the current allocation, the runtime grabs a bigger chunk of memory, copies everything over, updates every pointer that referenced the old location, and continues as if nothing happened. The stack typically doubles each time. The garbage collector also shrinks it back when the goroutine is done being dramatic.
You rarely notice any of this. When you finally do exceed the limit, there is no graceful fallback.
The Numbers That Matter
| Limit | Value |
|---|---|
| Initial goroutine stack | ~2 KB |
| Default maximum per goroutine | 1 GB (64-bit), 250 MB (32-bit) |
| Configurable via | runtime/debug.SetMaxStack(bytes int) int |
Catchable with recover()? | No |
The 1 GB cap is per goroutine, not per process. A simple recursive function with minimal local variables uses roughly 64 bytes per frame, putting the practical ceiling around 16 million frames. For interview inputs, trees with 10,000 nodes or graphs with 50,000 vertices, Go handles them without a complaint.
Compare that to Python collapsing at 1,000 frames by default. Or Java, whose thread stacks sit around 512 KB to 1 MB and give out somewhere between 8,000 and 12,000 frames. C++'s process stack on Linux is 8 MB. Go's 1 GB default is a completely different scale.
When Your Program Dies and Doesn't Come Back
If you do exceed the limit, Go does not throw an exception. It does not panic in the recoverable sense. The runtime prints a fatal error, terminates the program, and is completely unbothered by your feelings.
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc0200e0380 stack=[0xc020000000, 0xc040000000]
fatal error: stack overflow
runtime stack:
runtime.throw2({0x104a3a2e0, 0x12})
/usr/local/go/src/runtime/panic.go:1023 +0x40
...
The fatal error: stack overflow line is the one to recognize. The goroutine is dead, and any deferred recover() will not run because executing recover() requires stack space that no longer exists. That detail trips people up. recover() is not a magic force field around your function. It works for panics that happen within the Go runtime's normal flow. A fatal stack overflow is outside that flow entirely.
Python's RecursionError is a normal exception. You can catch it. Java's StackOverflowError extends Error and is technically catchable, though the docs politely suggest you not try. In Go, the answer is final. Program terminated. Have a nice day.
The Compiler Isn't Going to Save You Here
This one trips people up in interviews. You write a clean tail-recursive solution, feel good about it, and assume the compiler handles the rest.
Go does not perform tail call optimization. Every recursive call allocates a new stack frame, even when the recursive call is the last thing in the function and the compiler could, in theory, just reuse the current frame. It doesn't.
// Looks tail-recursive. In Go, it still consumes one frame per call. func sum(n int, acc int) int { if n == 0 { return acc } return sum(n-1, acc+n) }
TCO is the mechanism that turns tail recursion from O(n) space into O(1) space. Without it, sum(1_000_000, 0) allocates 1 million frames. You will not hit the 1 GB ceiling on simple integer accumulation, but do not assume Go frees those frames as you go. It does not.
Python deliberately skips TCO too. Among mainstream languages, Kotlin's tailrec keyword and Scheme are the notable exceptions. The Go team has been asked about this many times. The short answer is that TCO complicates stack traces and debugging. They consider the tradeoff not worth it.
Changing the Limit with SetMaxStack
If you genuinely need to raise the ceiling, the runtime/debug package has a knob.
import "runtime/debug" func main() { debug.SetMaxStack(2 << 30) // 2 GB // ... }
SetMaxStack returns the previous limit so you can restore it, and it affects the entire process, not just the calling goroutine. Setting it to 0 disables the limit entirely. Do not do that in production. You have been warned.
In an interview you will not touch this. Knowing the function exists is enough. It shows you understand that Go's 1 GB ceiling is a configured parameter, not a law of nature. The real-world uses for it are narrow: parsers for deeply nested documents, validators for circular data structures, things where call depth is legitimately large and you have actually measured it.
When This Comes Up in an Interview
Degenerate tree or graph inputs
A binary tree that is secretly a sorted linked list has depth equal to its length. DFS on a 100,000-node degenerate tree makes 100,000 recursive calls. In Go, that is roughly 6 MB of stack. Fine. You will not crash.
In Python, you crash at depth 1,000. In Java you crash past 8,000-ish. If an interviewer asks whether your recursive solution could fail, the honest answer for Go is: not for any input size typical in interviews, but you would want the iterative version for unbounded production inputs. That answer is better than "it might overflow" (it won't, in Go) or "I'm not sure" (you should be).
The "what if the input is huge" question
Your answer for Go has three parts. First, Go's goroutine stack grows automatically up to 1 GB, so a million-node traversal is not going to overflow. Second, the recursive version still uses O(n) stack space, same complexity as an explicit stack on the heap. Third, if you need predictable memory behavior or want to handle truly unbounded depth, convert to iteration.
Give all three parts. Stopping at "Go handles it fine" sounds like you do not know about the space cost.
Converting to iteration when you want to be safe
When you want the iterative version, use an explicit stack backed by a slice.
func dfsIterative(root *TreeNode) []int { if root == nil { return nil } result := []int{} stack := []*TreeNode{root} for len(stack) > 0 { node := stack[len(stack)-1] stack = stack[:len(stack)-1] result = append(result, node.Val) if node.Right != nil { stack = append(stack, node.Right) } if node.Left != nil { stack = append(stack, node.Left) } } return result }
The slice lives on the heap. You have traded call-stack frames for heap allocations, and the depth ceiling is gone. Memory usage is O(max depth) for DFS, same asymptotic complexity as the recursive version, but without the ceiling. For a full treatment of the conversion pattern, see converting recursion to iteration.
Go vs. Every Other Interview Language
| Language | Initial Stack | Default Depth Ceiling | Overflow Behavior | TCO? |
|---|---|---|---|---|
| Go | ~2 KB (grows) | ~16M simple frames | Fatal, unrecoverable | No |
| Python | Process stack | ~1,000 frames | RecursionError (catchable) | No |
| Java | ~512 KB (thread) | ~8,000-12,000 frames | StackOverflowError (catchable) | No |
| C++ | ~8 MB (Linux) | ~40,000-100,000 frames | Undefined behavior | Partial (GCC -O2) |
| JavaScript | ~1 MB (Node.js) | ~10,000-15,000 frames | RangeError (catchable) | Partial |
Go's dynamic stack is the most generous default by a large margin. When you finally do exceed it, the crash is fatal and unrecoverable. If you are practicing in Go and find yourself worrying about recursion depth, you are probably solving a problem where the recursive solution is fine and no other interview language would have survived it better anyway.
The thing that distinguishes a candidate who has actually written Go from one who has just read about it is knowing both halves of this: why Go can afford to be so generous (dynamic growth), and where the hard stop is (fatal, no recovery). Explaining that clearly under interview pressure is harder than it sounds. SpaceComplexity runs voice-based mock interviews that push on exactly this kind of runtime-behavior reasoning, with rubric feedback on how clearly you communicate it.
For Go-specific interview prep, see Go for coding interviews and Go coding interview gotchas. For the mechanics of why call stacks overflow at all, what causes a stack overflow covers the runtime model that Go's dynamic stack is designed to avoid bumping into.
Further Reading
- runtime/debug package, official documentation for
SetMaxStackand other runtime controls - Defer, Panic, and Recover (Go Blog), the official explanation of Go's panic and recover mechanism, and why fatal errors fall outside it
- Go Wiki: PanicAndRecover, community documentation on what can and cannot be recovered in Go
- Go runtime/stack.go source, the runtime source defining
_StackMin, maximum stack sizes, and the growth algorithm - Wikipedia: Stack-based memory allocation, background on how call stacks work at the OS and hardware level, which contextualizes why dynamic growth requires copying