How it works
The explanation, with examples you can run.
What a cycle costs
The work of marking is proportional to the live heap (plus the stacks and globals it has to scan), not to how much garbage there is. So the cost of collecting depends on how often cycles run, and that is what GOGC controls:
heap target = live heap + (live heap + GC roots) × GOGC / 100With the default GOGC=100 and 100 MiB live, the next cycle starts when the heap reaches about 200 MiB. GOGC=200 lets it reach about 300 MiB and runs about half as many cycles; GOGC=50 uses less memory and more CPU. GOGC=off disables collection unless a memory limit is set.
You can watch it:
$ GODEBUG=gctrace=1 ./appgc 1 @0.004s 2%: 0.22+0.49+0.038 ms clock, ... 3->5->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 PRead it as: the first cycle, 4 ms after start, 2% of CPU used by GC so far; wall time of the short pause, the concurrent mark and the second pause; the heap at the start of the cycle, at the end, and live afterwards; and the goal for the next one.
GOMEMLIMIT
A proportional target has a weakness: a brief spike in live data doubles the heap target too, and in a container that can mean being killed for running out of memory. Go 1.19 added a memory limit (GOMEMLIMIT=900MiB or debug.SetMemoryLimit) covering all memory the runtime manages, not just the heap. As usage approaches it, the collector runs more often regardless of GOGC.
The limit is soft. If the live heap really needs more, the runtime would rather exceed the limit than spend all its time collecting: it caps GC CPU at roughly 50% over a time window. A common setup for a service with a container limit is the default GOGC plus GOMEMLIMIT about 5–10% below the container's limit. Some teams also use GOGC=off with a limit, so the program only collects as it gets close to the ceiling. That is efficient, but only safe if nothing else shares the memory.
debug.SetGCPercent(100) // same as GOGC=100, the defaultdebug.SetMemoryLimit(64 << 20) // same as GOMEMLIMIT=64MiBGreen Tea (Go 1.26)
Go 1.25 shipped an experimental collector called Green Tea, and Go 1.26 made it the default. Instead of putting individual objects on its work list, it tracks and scans whole 8 KiB pages of small objects, which uses the CPU cache far better and scales better across cores. The Go team reports that many workloads spend around 10% less time in GC, and some up to 40%. Nothing changes in how you tune it: GOGC and GOMEMLIMIT work the same way. In Go 1.26, building with GOEXPERIMENT=nogreenteagc turned it off; the release notes said that opt-out was expected to go in Go 1.27.
Reducing GC work
The settings only trade memory for CPU. To reduce the total work, allocate less: avoid escapes on hot paths, preallocate slices whose size you know, reuse buffers (a sync.Pool for short-lived objects), and keep long-lived data pointer-free where you can, since the collector doesn't need to scan memory that contains no pointers. Profile allocations with go test -bench . -benchmem or the heap profile in net/http/pprof before changing anything.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.