GOMEMLIMIT for a low-traffic Go container: choosing a value without automatic cgroup v2 detection
0 reputation · 06 Oct 2021, 20:16 UTC
0 reputation · 06 Oct 2021, 20:16 UTC
I run a low-traffic Go HTTP service in a cgroup v2 container (Kubernetes, memory limit 256Mi) and want to shrink its idle memory footprint to cut costs. Go 1.19+ offers GOMEMLIMIT as a soft memory limit that makes the GC more aggressive, but the runtime does not automatically read cgroup v2 memory.max to populate it.
Because detection is manual, the value must stay synchronized with the deployment manifest. The common heuristic is 80–90% of the container limit, but GOMEMLIMIT is soft: allocation bursts can exceed it, and setting it too low increases GC CPU usage, which could offset the savings. It also does not cover CGO allocations or runtime metadata. I can observe /gc/heap/goal:bytes and /memory/classes/total:bytes via runtime/metrics to confirm the effect.
35275 reputation · 07 Oct 2021, 04:54 UTC
For a mostly idle Go service under a 256Mi cgroup v2 limit, start with GOMEMLIMIT=192MiB (75% of the container limit). This is below the commonly cited 80–90% heuristic, but that heuristic assumes steady allocation pressure; an idle service has little live heap, so a lower soft limit shrinks RSS further while the GC still has plenty of headroom before CPU overhead becomes visible. Verify with runtime/metrics, and derive the value at startup from /sys/fs/cgroup/memory.max if you want it to track the manifest automatically.
The tradeoff is simple: lowering GOMEMLIMIT makes the GC run more often, shrinking the heap (and eventually RSS, since the runtime returns memory to the OS) at the cost of GC CPU. For an idle service the live heap is small, so the GC can hold a low target with few cycles — that is exactly the case where aggressive settings are cheap.
GOMEMLIMIT=192MiB./sched/gc:cpu_fraction. If it stays under roughly 5%, you have room to go lower; step down in 25MiB increments./gc/heap/goal:bytes is pinned at the limit while the live heap keeps pushing against it, you have gone too far — step back up.Remember the soft-limit caveat: a burst can temporarily exceed GOMEMLIMIT, and it does not account for CGO allocations, goroutine stacks, or other runtime metadata. Keep enough gap to the 256Mi cgroup hard limit that a spike does not trigger the OOM killer — that gap is the real reason not to set GOMEMLIMIT at 95%+.
Yes, this is supported. debug.SetMemoryLimit has been available since Go 1.19, so you can read the cgroup v2 limit yourself and set the soft limit programmatically:
data, err := os.ReadFile("/sys/fs/cgroup/memory.max")
if err == nil {
s := strings.TrimSpace(string(data))
if s != "max" {
if limit, err := strconv.ParseInt(s, 10, 64); err == nil {
debug.SetMemoryLimit(limit * 3 / 4)
}
}
}Handle the "max" case (no limit set) by falling back to a default or leaving the limit unset. This approach keeps the value synchronized with the manifest — a real advantage over a static env var, which can silently drift when someone edits the Deployment's memory limit. The tradeoff is that env-var configuration is more visible to operators and easier to override per environment. Either is defensible; startup derivation is the better default if your platform team resizes containers often.
Scrape these via runtime/metrics (or expvar/a metrics exporter) before and after the change:
/gc/heap/goal:bytes — the GC's target heap size. After the change it should be capped near your GOMEMLIMIT rather than floating at 2× live heap (the default GOGC=100 behavior)./memory/classes/heap:bytes (or /memory/classes/total:bytes for the full picture including stacks and metadata) — confirms actual heap stays near the goal and total runtime memory stays comfortably under 256Mi./sched/gc:cpu_fraction — the cost side of the tradeoff. This is the number that tells you whether your memory savings are being paid for in CPU.One missing detail that would change the recommendation: your traffic's burst profile. If "low traffic" means occasional sharp spikes rather than uniformly low load, stay closer to 80–85% (205–218MiB) and accept less RSS reduction, because the OOM risk during a burst outweighs idle savings.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.