Go

How do GOGC and GOMEMLIMIT interact, and how would you tune the GC for a latency-sensitive service?

Question 394HardGo 1.22 to 1.25

Go's GC is a concurrent, non-generational, non-moving mark-sweep collector. Pacing:

  • GOGC (default 100): the next GC triggers when the heap grows by GOGC% over the live heap after the last GC. Live 200 MB → next GC around 400 MB. Higher GOGC = fewer GCs, more memory. GOGC=off disables proportional pacing.
  • GOMEMLIMIT (Go 1.19+): a soft limit on total Go runtime memory. As usage approaches it, the GC runs more often regardless of GOGC.

A popular pattern for containers: GOGC=off GOMEMLIMIT=~90% of container limit — no GCs until you approach the limit. Risk: if the live heap itself approaches the limit, the GC runs continuously (a "death spiral"); the runtime caps GC CPU at ~50% to mitigate this, but you'll see latency collapse. Safer: keep GOGC=100 (or 200) and set GOMEMLIMIT as a backstop.

import "runtime/debug"

func init() {
    debug.SetGCPercent(200)
    debug.SetMemoryLimit(900 << 20) // 900 MiB, for a 1 GiB container
}
// Observe: GODEBUG=gctrace=1, runtime/metrics "/gc/cycles/total:gc-cycles",
// "/cpu/classes/gc/total:cpu-seconds"

Tuning GC is secondary; the primary lever is allocating less (alloc_space profile, preallocation, avoiding interface boxing and pointer-heavy structures, which also reduce marking work). Go 1.25 shipped the experimental "Green Tea" GC (GOEXPERIMENT=greenteagc), which improves marking locality for small objects, and Go 1.26 made it the default. Since Go 1.25 the runtime is also container-aware: GOMAXPROCS respects cgroup CPU limits by default.

More on Performance, Profiling & Testing

All 38 Performance, Profiling & Testing questions