skip to content

How does a goroutine's stack grow when its initial ~2 KB is not enough?

level: juniorimportance: should knowfreq 58%

answer

  1. it is decided before the body runs
  2. there is no second segment
  3. double, copy, fix up, resume
  4. the runtime entry point is morestack

basics

~20 s

Each Go function starts by checking whether enough stack space is left. If not, it calls into the runtime, which allocates a new stack of double the size, copies the old stack into it, and lets the function continue.

solid answer

~40 s

A goroutine starts with a small stack, about 2 KB, and the runtime grows it on demand. The compiler puts a check in the prologue of almost every function: compare the stack pointer against the goroutine's stack guard, and if the frame would not fit, call `runtime.morestack`. That lands in `runtime.newstack`, which allocates a new stack twice the current size, copies the whole old stack into it with `copystack`, fixes up every pointer that referred to the old stack, frees the old one, and re-runs the function prologue. The stack stays one contiguous block; Go does not chain segments together. Growth is therefore invisible to your code, costs an amortised constant per call, and continues until the per-goroutine limit (1 GB on 64-bit) is hit, at which point the runtime kills the process.

code

go · 5 lines
go
func hash(chunk []byte) [32]byte {
	var scratch [64 * 1024]byte // 64 KB of stack, wanted all at once
	n := copy(scratch[:], chunk)
	return sha256.Sum256(scratch[:n])
}

go deeper

for a junior

Recall the shape of the answer: goroutines start with a very small stack and the runtime enlarges it automatically when a call needs more room. Say that the check happens on function entry, not on a hardware fault.

for a middle

Explain the mechanics: the prologue compares against the stack guard, morestack allocates a stack of double the size, the old contents are copied, and pointers into the stack are fixed up before execution resumes.

for a senior

Show you know the cost model and the limits: doubling makes growth amortised constant, huge local arrays force immediate growth, and the per-goroutine ceiling is 1 GB on 64-bit with a fatal error, not a panic, at the top.

for a principal

Frame it as the design choice it is: small growable stacks are what makes a goroutine-per-request model affordable, and the tradeoff paid for it is a runtime that must be able to relocate stacks, which constrains what unsafe code and foreign code may hold.

## The problem growable stacks solve Every function call needs space for its local variables, its spilled arguments and its return address. That space lives on the stack of whatever is executing the call. A native OS thread solves this by reserving a large fixed range up front — often 1 MB or 8 MB of virtual address space — and simply never letting the program exceed it. That is fine when you have dozens of threads, and hopeless when you want hundreds of thousands of concurrent tasks: you would either reserve far more address space than you can afford, or pick a small fixed size and crash on any moderately deep call chain. Go takes the other route. A goroutine starts with a **tiny stack — about 2 KB** — and the runtime **grows it on demand**, so the depth a goroutine can reach is not a decision anybody makes at creation time. ## The check in every function prologue The Go compiler emits a preamble at the top of nearly every function it compiles. Conceptually it is: *is the stack pointer, minus the number of bytes this frame needs, still above the goroutine's stack guard?* The guard is a field on the goroutine's runtime descriptor (`stackguard0`) that sits a small distance above the bottom of the currently allocated stack. If the frame fits, execution falls straight through into the function body and the cost is a compare and a not-taken branch. If it does not fit, the prologue jumps to `runtime.morestack`. A few functions opt out of the check with the `//go:nosplit` directive — mostly deep runtime internals that cannot themselves trigger a stack growth. Those functions must fit inside a small reserved margin below the guard, and the linker reports an error if the chain of nosplit calls would overflow it. ## What growth actually does `runtime.morestack` calls `runtime.newstack`, which: 1. Allocates a **new stack of double the current size** (2 KB becomes 4 KB, then 8 KB, and so on). 2. Calls `runtime.copystack`, which copies the entire used portion of the old stack into the new one — byte for byte, at a different address. 3. **Adjusts every pointer** that referred to the old stack. The compiler records, for each frame, exactly which words are pointers, so the runtime can walk the frames and add the address delta to each such word, including pointers held in deferred-call records. 4. Frees the old stack back to the runtime's stack allocator. 5. Returns to the prologue, which re-runs the check — this time it passes — and the function proceeds. From your code's point of view nothing happened except that the call took longer. Nothing is copied that the program can observe, because the runtime has fixed up all the internal references. ## Why one contiguous stack instead of segments An earlier design chained a fresh segment onto the old one when space ran out, which avoided copying. It had a famous pathology usually called the **hot split**: if the boundary between two segments fell inside a hot loop, every iteration would allocate a segment on the way in and free it on the way out, turning a cheap call into an allocation. Copying to a single larger contiguous stack removes that cliff. Because each growth **doubles** the size, a goroutine that ends up N bytes deep pays O(N) total copying across its life — amortised constant per call — and never oscillates at a boundary. ## What forces growth Deep call chains are the obvious cause, and recursion is the classic one. Large frames matter just as much: a function that declares a `[64 * 1024]byte` local needs 64 KB of stack all at once, so calling it from a fresh goroutine forces several doublings immediately. This is one reason big buffers are usually heap-allocated or pooled rather than declared as huge local arrays. ## The ceiling Growth is not unlimited. The default maximum size for a single goroutine's stack is **1 GB on 64-bit platforms** (250 MB on 32-bit), adjustable with `runtime/debug.SetMaxStack`. Exceeding it is not a recoverable panic: the runtime prints `runtime: goroutine stack exceeds 1000000000-byte limit`, then `fatal error: stack overflow`, and the whole process dies. In practice, code that reaches the limit is code with unbounded recursion, and the fix belongs in the code, not in the limit.

  • Why does Go copy the whole stack instead of chaining a new segment onto the old one?
    Segmented stacks suffer the hot-split problem: if a segment boundary lands inside a hot loop, every iteration allocates a segment on entry and frees it on exit, so a cheap call becomes an allocation. A single contiguous stack that doubles on growth removes that cliff and makes the amortised cost per call constant.
  • What does the compiler emit at the top of a Go function to make growth possible, and which functions skip it?
    It emits a prologue that compares the stack pointer against the goroutine's stack guard and jumps to `runtime.morestack` if the frame will not fit. Functions marked `//go:nosplit` skip the check, so they must fit within a small reserved margin; the linker fails the build if a chain of nosplit calls could overflow it.
  • Does stack growth cost anything for code that never recurses?
    Almost nothing. A goroutine that stays shallow pays one compare-and-branch per call and never grows past its initial size. The copying cost only appears when the depth or the frame sizes actually exceed what is allocated, and because each growth doubles the stack, it happens a logarithmic number of times.

Like outgrowing a notebook: instead of taping a second notebook to the first, you buy one twice the size and recopy everything, so the notes stay in one continuous run.

saying these in an interview costs you the question

  • Says a goroutine's stack is fixed at 2 KB forever
  • Claims goroutines get 1 MB stacks like OS threads
  • Thinks growth links a new segment onto the old stack
  • Says the stack grows by one fixed-size page each time
  • Believes growth happens lazily on a page fault from the OS