skip to content

Tuning Under Container Limits

The three knobs you actually set in production, GOGC, GOMEMLIMIT and GOMAXPROCS, each reconciled with the cgroup memory and CPU limits the container around the process enforces.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

15

What does the GOGC environment variable control in a Go program, and what does its default of 100 mean?

level: juniorimportance: must knowfreq 55%

answer

  1. a percentage, not a byte count
  2. measured against what survived the last cycle
  3. 100 means grow by 100 percent
  4. double the live set, then collect
  5. read from the environment at start-up

basics

~20 s

GOGC sets how much the heap may grow between garbage collections, as a percentage of the live data kept after the last one. The default, 100, lets the heap roughly double before the next collection starts.

solid answer

~40 s

GOGC is a growth percentage, not a size. The runtime aims to start the next collection when the heap has grown by GOGC percent beyond the live data that survived the previous cycle, so GOGC=100 means "let the heap roughly double", GOGC=50 means "let it grow by half", and GOGC=400 means "let it grow to five times the live set". It is read from the environment when the process starts, and `GOGC=off` disables automatic collection entirely. Because it is a ratio, it puts no absolute cap on memory: if the live set doubles, the peak heap doubles with it at the same GOGC. Raising it buys fewer collections at the cost of a larger resident heap; lowering it does the reverse.

go deeper

for a junior

Be ready to state that GOGC is a percentage of the live heap, that the default is 100 meaning "let the heap roughly double", and that higher uses more memory and less CPU while lower does the opposite.

for a middle

Explain the arithmetic out loud with a concrete live-set number, and be able to say why the same GOGC gives a bigger peak heap when the live set grows. Know that the variable is read only at start-up.

for a senior

An interviewer expects you to reject "GOGC caps memory" immediately and to connect the value to a real workload shape — how a spiky service's peak heap moves with it, and what you would measure before changing it.

for a principal

Own the framing that GOGC is a conversion rate between memory and CPU, and that the right value is a budget decision made with numbers, not a default copied from a blog post.

## The one knob everyone meets first `GOGC` is the Go runtime's primary garbage-collection tuning knob. It is read from the environment once, when the process starts, and it holds a single integer: **a percentage describing how much the heap is allowed to grow before the next collection cycle begins.** ### The percentage is relative, not absolute After a collection finishes, the runtime knows how much data is still reachable — the **live set**. `GOGC` says how much *additional* memory may be allocated on top of that before the collector starts again: ``` next collection target ~= live set * (1 + GOGC/100) ``` With a 200 MB live set: - `GOGC=100` (default) — collect at roughly 400 MB (grow by 100%, i.e. double). - `GOGC=50` — collect at roughly 300 MB (grow by 50%). - `GOGC=400` — collect at roughly 1 GB (grow to five times the live set). - `GOGC=off` — never collect automatically at all. (In modern Go the target also accounts for goroutine stacks and globals, but the useful mental model is "live heap times one plus GOGC over one hundred".) ### What follows from it being a ratio The most common misunderstanding is treating `GOGC` as a memory limit. It is not: - **It sets no ceiling.** If your live set grows from 200 MB to 2 GB because you now cache more, the same `GOGC=100` will let the heap reach about 4 GB. The knob controls headroom, never the live data itself. - **It scales with load shape.** A service whose live set balloons during a spike gets a proportionally bigger peak heap during that spike. - **It is not a pause-time goal.** Go's collector is a concurrent, non-generational mark-sweep collector; stop-the-world phases are short and are not what `GOGC` tunes. `GOGC` decides *how often* cycles run and *how much garbage* is allowed to pile up between them. - **It is not a generation size.** Go has no young/old generations, so there is nothing analogous to sizing a nursery. ### Direction of the tradeoff A higher `GOGC` means more allocation is permitted between cycles, so cycles run less often and the collector consumes less CPU — while the process holds more memory. A lower `GOGC` means the opposite: a tighter heap, and a collector that runs far more often and takes a bigger share of CPU. That single sentence is the whole reason the knob exists: **it converts memory into CPU, in both directions.** ### GOGC=off `GOGC=off` disables the automatic trigger. The heap then only grows: every allocation adds to it and nothing is ever reclaimed unless the program explicitly calls `runtime.GC()`. It is defensible for a short-lived process that allocates a bounded amount and exits — a one-shot CLI, a compiler-like batch step — and it is a hazard for anything long-lived, because the process will keep growing until the operating system stops it. ### Changing it The environment variable is consulted at start-up, so exporting a new value or calling `os.Setenv` later has no effect on a running process. The runtime equivalent is `runtime/debug.SetGCPercent`, which takes the same percentage and applies it immediately. ### How to talk about it in an interview Say the number is a **percentage of the live set**, give one worked arithmetic example, name the default (100), and state the tradeoff direction. Then be explicit that it is not a memory cap — that distinction is what separates a candidate who has read the variable's name from one who has tuned a service with it.

  • Does GOGC put an upper bound on how much memory a Go process can use?
    No. It is a ratio applied to the live set, so it bounds headroom, not total memory. If the live set grows, the peak heap grows in proportion at the same GOGC. Capping absolute memory needs a different mechanism entirely; lowering GOGC only squeezes the garbage headroom and does nothing about the data that is genuinely still reachable.
  • What does GOGC=off actually do, and when is it defensible?
    It disables the automatic collection trigger, so the heap only grows; nothing is reclaimed unless the program calls runtime.GC() itself. It is reasonable for a short-lived process with a bounded allocation total — a one-shot CLI or batch step that exits quickly — and reckless for a long-running server, which will simply keep growing until the operating system kills it.
  • If a service's live set doubles overnight, what happens to its peak heap at an unchanged GOGC=100?
    It roughly doubles as well. The target is computed from the live set each cycle, so twice as much reachable data means twice as much allowed headroom on top of it. This is why a cache that quietly grows can double a service's memory footprint without anyone changing a single tuning value.

It is a restocking rule, not a warehouse size: "let the shelves fill to twice what is actually being used before we do a clear-out." If genuine stock doubles, the warehouse you need doubles too — the rule never caps the building.

saying these in an interview costs you the question

  • Calls GOGC a memory limit or a maximum heap size
  • Thinks GOGC is measured in megabytes rather than percent
  • Says GOGC=0 disables the collector (it does not; a negative value does)
  • Describes GOGC as a pause-time target
  • Assumes exporting GOGC changes a process that is already running
  • Talks about young and old generations, which Go's collector does not have
open as a page

What does GOMAXPROCS control, and how does a container's CPU limit affect its default?

level: juniorimportance: must knowfreq 58%

basics

~20 s

GOMAXPROCS caps how many goroutines run Go code simultaneously. Since Go 1.25 on Linux its default is the lower of the machine's usable logical CPUs and the container's cgroup CPU limit; older releases looked only at the CPU count.

open as a page

What does Go's GOMEMLIMIT environment variable do, and why is it a soft limit?

level: juniorimportance: must knowfreq 34%

basics

~20 s

GOMEMLIMIT sets a target ceiling on the total memory the Go runtime manages, so the garbage collector runs more often as usage approaches it. It is soft: the runtime never refuses an allocation, so a program can still exceed it.

open as a page

Why does raising GOGC from 100 to 400 cut GC CPU time but raise a Go service's peak memory?

level: middleimportance: must knowfreq 62%

basics

~20 s

A collection cycle's cost tracks live data, not garbage, so quadrupling the growth allowance runs about a quarter as many cycles for the same allocation rate. The price is the heap the process holds between cycles, which grows roughly fourfold.

open as a page

Why can a Go service be OOM-killed with GOMEMLIMIT set to the container's memory limit?

level: seniorimportance: must knowfreq 52%

basics

~10 s

GOMEMLIMIT governs only memory the Go runtime manages, and it is a target rather than a cap. The binary, cgo allocations and mapped files sit outside it, so an equal setting leaves no headroom.

open as a page

How does Go 1.25's container-aware GOMAXPROCS default pick its value, and when is it recomputed?

level: middleimportance: should knowfreq 40%

basics

~20 s

On Linux the Go 1.25+ runtime takes the smaller of the usable logical CPUs and the cgroup CPU limit, rounds a fractional limit up, and stays at 2 or above. It re-reads the limit periodically.

open as a page

Why do teams pair GOGC=off with GOMEMLIMIT, and when does that combination bite?

level: middleimportance: should knowfreq 45%

basics

~20 s

Turning the growth trigger off leaves GOMEMLIMIT as the only reason a collection starts, so a service uses the whole budget it was granted. The cost is a heap that lives at the ceiling, with no ramp before constant collection.

open as a page

A report service runs with GOGC=25 to hold memory down, and now burns a third of its CPU collecting during its hourly spike. How do you confirm that and choose a better value?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Measure collection CPU over the spike window alone, not since process start: diff the runtime's GC CPU-seconds metric across the spike. Then replay that spike at higher GOGC values and take the largest peak heap you can afford.

open as a page

A Go sidecar with GOMAXPROCS=64 runs under a half-core quota and stalls in bursts. How do you confirm the cause?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Compare the runtime's P count with the CPU quota: read runtime.GOMAXPROCS(0) and runtime.NumCPU() from inside the container and check the cgroup's throttle counters. Stalls that align with quota periods, not with a hot function, mean the runtime is over-subscribing its allowance.

open as a page

How do you decide a Go service's GOGC value when the platform team wants 30% less memory per replica?

level: principalimportance: should knowfreq 33%

basics

~20 s

Treat GOGC as a measured exchange rate between two billed resources. Replay peak load at several values, chart peak heap against collection CPU, pick the knee that still meets the latency objective, and keep the value operator-settable.

open as a page

How do you decide what GOMEMLIMIT a service gets, and whether being killed beats thrashing?

level: principalimportance: should knowfreq 42%

basics

~10 s

Treat GOMEMLIMIT as splitting a fixed allowance between Go-managed and other memory, then choose a failure: a soft ceiling buys degradation instead of a kill, which pays only when a restart is expensive.

open as a page

How does runtime/debug.SetGCPercent differ from setting GOGC, and what does it return?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

GOGC is read from the environment once at start-up; debug.SetGCPercent changes the same target percentage in a running process, takes effect immediately, and returns the previous setting so it can be restored. A negative value disables collection.

open as a page

What does calling runtime.GOMAXPROCS(n) at start-up turn off, and how do you get the default back?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

Setting GOMAXPROCS explicitly — from code or from the environment variable — pins the value and stops the runtime's container-aware updates. runtime.GOMAXPROCS(0) only reports the current value, and runtime.SetDefaultGOMAXPROCS() hands control back to the runtime.

open as a page

After lowering GOMEMLIMIT, a Go service stops crashing but pins CPU and stalls — what is happening?

level: seniorimportance: nice to knowfreq 33%

basics

~10 s

Live data now sits just under the ceiling, so the collector restarts almost as soon as a cycle ends. Because GOMEMLIMIT is soft the process survives, but collection takes the CPU and throughput collapses.

open as a page

As platform owner, how do you set a fleet-wide GOMAXPROCS policy for Go services in containers?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Make the runtime's container-aware default the fleet norm, enforce it with a minimum go.mod language version and a ban on code overrides, require a start-up log of the chosen value, and let a service override only with measurements and an owner.

open as a page