— notes
How Goroutine Stacks Grow and Shrink
If you ask any mid-senior Go dev what makes goroutines lightweight you’ll get the reply:
They start with 2 KB of stack instead of 1 MB like OS threads.
They’re not wrong. But they’re not thinking deep enough.
Go’s stack model isn’t just a small preallocated buffer; it’s a live, evolving region of memory that resizes in realtime, grows when needed, and shrinks.
Go’s runtime gives each new goroutine 2 KB of stack. But Go doesn’t panic when you blow past it – it grows the stack dynamically by allocating a new region (typically doubling the size) and copying the old stack frames over.
Stack Growth Lifecycle
graph LR
A["New Goroutine\n2 KB stack"] --> B["Function\nCall"]
B --> C{"Stack\nCheck"}
C -->|Enough| D["Continue\nExecution"]
C -->|Overflow| E["Allocate\n2x Stack"]
E --> F["Copy Old\nFrames"]
F --> G["Update\nPointers"]
G --> DEach goroutine has an upper stack limit of about 1 GB. Hit it and the program panics:
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflowStack Size Progression
graph TD
S1["2 KB\n(initial)"] --> S2["4 KB"]
S2 --> S3["8 KB"]
S3 --> S4["16 KB"]
S4 --> S5["32 KB"]
S5 --> S6["..."]
S6 --> S7["~1 GB\n(ceiling)"]
S7 -.->|"GC shrink\n(if idle + mostly unused)"| S5
S5 -.->|"GC shrink"| S3Contiguous Stack Copy
When a stack outgrows its current allocation, Go allocates a new contiguous block at double the size, copies all frames, and adjusts every internal pointer.
graph LR
subgraph "Old Stack (4 KB)"
O1["Frame 1"]
O2["Frame 2"]
O3["Pointer A\n→ Frame 1"]
end
subgraph "New Stack (8 KB)"
N1["Frame 1\n(copied)"]
N2["Frame 2\n(copied)"]
N3["Pointer A'\n→ Frame 1\n(adjusted)"]
N4["Free Space"]
end
O1 -->|copy| N1
O2 -->|copy| N2
O3 -->|"adjust + copy"| N3Stack growth can trigger GC pressure even without heap allocations. A runaway stack holds pointers that get scanned by GC. This is how your 5ms p99 turns into 100ms.
There’s no tuning knob for stack size. No config. No CLI flag. The only way to manage it is through code discipline: avoid recursive goroutines unless they terminate quickly, don’t hold large structs deep in call graphs, and never assume goroutines are free.