When does Go's runtime shrink a goroutine's stack after it has grown?
answer
- growth is eager, the other direction is not
- it happens while something else is already walking the frames
- a ratio decides it, and the step is a halving
- there is a floor at the starting size
basics
~20 sOnly during garbage collection, when the runtime scans that goroutine's stack. If less than a quarter of the stack is in use, the runtime halves it by copying the live part into a smaller stack, never going below the ~2 KB minimum.
solid answer
~40 sShrinking is not eager. A goroutine that recursed deeply keeps its enlarged stack until a garbage-collection cycle scans it; at that safe point the runtime checks how much is actually in use, and if it is under a quarter of the allocated size, it copies the live portion into a stack half the size — the same copy-and-fix-up machinery used for growth, run in the other direction. It never shrinks below the roughly 2 KB minimum, and it skips goroutines it cannot safely relocate at that moment, such as one blocked in a system call. The operational consequence: after a burst of deep work, stack memory stays held until the next GC, and with tens of thousands of long-lived goroutines that shows up in `runtime.MemStats.StackInuse` rather than in heap numbers.
go deeper
Recall that a goroutine's stack can get smaller again, but not immediately — the runtime does it during garbage collection, not when the deep calls return.
Explain the rule and the machinery: under a quarter in use means halve it, using the same copy-and-fix-up path as growth, with a floor at the starting size.
Show the operational reading: stack bytes appear in MemStats.StackInuse rather than in a heap profile, an idle process may hold enlarged stacks for a long time, and long-lived goroutine pools multiply the effect.
Treat it as a capacity question: decide whether your service's memory model tolerates goroutines that each retain their deepest frame, and whether the answer is bounded depth, shorter-lived goroutines, or simply budgeting for it.
## Growth is one-way until the collector says otherwise When a goroutine needs more stack, the runtime doubles it immediately — that has to be synchronous, because the function cannot run otherwise. Giving the space back is the opposite: nothing forces it, so Go does it opportunistically, at a moment when it is already walking the goroutine's frames. That moment is **garbage collection**. As part of a GC cycle the runtime scans each goroutine's stack to find pointers into the heap. Having stopped that goroutine at a safe point and walked its frames anyway, it can cheaply ask a second question: *is this stack much bigger than it needs to be?* ## The rule The check is a simple ratio. If the goroutine is using **less than a quarter** of its allocated stack, the runtime shrinks the stack to **half** its current size. Shrinking uses exactly the same machinery as growth: allocate the new (smaller) stack, copy the live region, walk the frames and adjust every pointer using the compiler's pointer maps, free the old stack. A stack that ballooned to 1 MB and then went idle does not drop straight back to 2 KB; it halves once per qualifying GC cycle, so it takes several cycles to come all the way down. There is a floor: the stack never shrinks below the minimum size a goroutine starts with, around 2 KB. And there are goroutines the runtime declines to shrink at that moment — most importantly ones the runtime cannot safely relocate right then, such as a goroutine sitting in a system call whose frames may be referenced by addresses the runtime does not control. Those simply keep their size until a later cycle. ## Why this matters operationally Three consequences show up in real services. **Stack memory is not heap memory.** The bytes held by goroutine stacks are reported separately: `runtime.MemStats.StackInuse` (in use by stacks) and `StackSys` (obtained from the OS for stacks). A service whose resident set climbs while heap profiles look flat may be holding stacks, not heap objects — and a heap profile will never show them, because a heap profile only samples heap allocations. **A spike in depth outlives the spike in work.** If a request handler recursed deeply for a few seconds, its goroutine keeps the enlarged stack until a GC cycle scans it. If the process is idle after the spike, GC may not run for a while — allocation is what paces it — so the memory sits there. This is a classic "why is RSS still high after the load stopped?" answer. **Many long-lived goroutines multiply it.** Ten thousand goroutines that each once needed 64 KB hold roughly 640 MB between them until they are shrunk or exit. Goroutines that finish give their stacks back immediately, which is one reason short-lived worker goroutines behave better here than long-lived ones that occasionally do something deep. ## What you can and cannot control You cannot ask the runtime to shrink a specific goroutine's stack. `runtime.GC()` forces a collection cycle, which will scan stacks and therefore may shrink them, but forcing GC to reclaim stack memory is a blunt tool and usually the wrong lever in production. The things that actually help are structural: - **Bound the depth**, so stacks never grow large in the first place. A recursive algorithm with a depth limit, or an iterative formulation, keeps every goroutine near the minimum. - **Do not declare very large local arrays** in functions that long-lived goroutines call; a single 1 MB local array forces the whole stack up to at least that size. - **Let goroutines exit.** A pool of permanent goroutines retains whatever their worst call chain needed; goroutines created per unit of work release their stacks when they return. ## The one-sentence version Stacks grow eagerly and shrink lazily: growth happens the instant a frame does not fit, shrinking happens only when the collector is already looking at the goroutine and finds under a quarter of the stack in use, at which point it halves it.
- A Go service's resident memory stays high after a burst of deep recursion, yet the heap profile looks flat. What would you check?Goroutine stack memory. Look at `runtime.MemStats.StackInuse` and `StackSys`, and at how many long-lived goroutines exist. Enlarged stacks are only halved when a GC cycle scans the goroutine, and GC is paced by allocation, so an idle process may hold them for a long time. A heap profile never shows stack bytes.
- Why does a goroutine that finishes give its stack back sooner than one that stays alive?When a goroutine exits, the runtime frees its stack immediately and returns it to the stack allocator — no GC cycle or usage ratio is involved. A long-lived goroutine keeps whatever its deepest call chain required until a collection scans it and finds under a quarter in use.
- Can you force a specific goroutine's stack to shrink?Not directly; there is no API for it. `runtime.GC()` triggers a cycle that scans stacks and may shrink eligible ones, but that is a process-wide hammer, not a per-goroutine control. The durable fixes are bounding recursion depth, avoiding very large local arrays, and letting goroutines exit.
saying these in an interview costs you the question
- Says a stack shrinks as soon as the deep calls return
- Claims stacks never shrink at all once grown
- Expects a shrink to go straight back to 2 KB in one step
- Looks for retained stack memory in a heap profile
- Thinks stack bytes are counted in the heap goal set by GOGC