How does GOMEMLIMIT change the heap goal Go's GC pacer computes, and why is it soft?
answer
- an absolute ceiling next to a ratio
- two candidate goals, take the smaller
- counts more than just the heap
- the runtime never refuses an allocation for it
- collection CPU has a cap of its own
basics
~20 sGOMEMLIMIT is a soft ceiling on the memory the Go runtime manages. The pacer computes a goal that keeps total runtime memory under it, then uses the smaller of that and the GOGC goal, so cycles grow more frequent near the ceiling.
solid answer
~50 s`GOMEMLIMIT` gives the pacer a second way to compute a heap goal. Alongside growing the live heap by `GOGC` percent, the runtime works out what goal keeps the total memory it manages - heap, goroutine stacks, globals and its own metadata - beneath the limit, and paces to whichever goal is smaller. As the live set approaches the ceiling, the limit-derived goal squeezes the effective growth ratio towards nothing and cycles become more frequent. It is *soft* in a precise sense: the runtime never fails an allocation or terminates the program to honour it. If it cannot stay under, it exceeds the limit - and a GC CPU limiter caps collection at 50% of CPU so a program is not pinned in an endless collection loop trying. It also covers only memory the runtime accounts for, so cgo mappings fall outside it.
code
text · 5 linesGOMEMLIMIT=4GiB ./controller # B, KiB, MiB, GiB, TiB are accepted
GOMEMLIMIT=4294967296 ./controller # bare numbers are bytes
# default is effectively unlimited; the same value can be set from
# inside the program with runtime/debug.SetMemoryLimitgo deeper
Know that GOMEMLIMIT is an absolute memory ceiling the collector aims to stay under, and that soft means the program keeps running rather than being killed if it cannot.
Be able to say that the pacer computes both a GOGC goal and a limit-derived goal and follows the smaller, and to list what the limit accounts for beyond the heap itself.
Show what the soft guarantee actually buys: as the live set approaches the ceiling, collection cost rises until the GC CPU limiter binds at 50% and the limit is breached instead. Know that breaching it is information about the live set.
Own the distinction between a limit that trades CPU for memory and one that trades availability for it, and be able to say what your service should do when the collector has already done all it can.
## The problem GOMEMLIMIT solves `GOGC` alone paces relative to the live heap. That is elegant and self-scaling, but it has no idea what machine the program is on. A ratio that behaves perfectly at a 100 MB live set will target a 4 GB heap once the live set reaches 2 GB, whether or not that much memory exists. `GOMEMLIMIT` adds the missing absolute number. ## How it enters the pacing calculation With a limit configured, the runtime computes two candidate goals for the next cycle: 1. The `GOGC` goal, roughly `live heap x (1 + GOGC/100)`. 2. A limit-derived goal: the largest heap that still leaves total runtime memory beneath `GOMEMLIMIT`, after subtracting everything else the runtime is holding. It paces to the **smaller** of the two. That single rule explains most of the behaviour people find surprising: - Well below the limit, the `GOGC` goal is smaller and the limit is invisible - it changes nothing at all. - As the live set grows, the limit-derived goal falls until it drops beneath the `GOGC` goal, and from that point the limit is setting the pace. The effective growth ratio shrinks continuously; cycles get closer together and each reclaims less headroom. - Both knobs remain live simultaneously. Setting a memory limit does not disable `GOGC`, and vice versa. ## What counts against the limit The limit is not a heap limit. It covers **all memory the Go runtime manages**: the heap, goroutine stacks, globals, and the runtime's own metadata and internal structures. That makes it a much better proxy for a process's real footprint than a heap figure alone. Equally important is what it does *not* cover: memory the runtime does not account for. Allocations made through cgo, mappings the program makes for itself, and the binary's own text and data are outside it. A process whose footprint is dominated by such memory will breach any container limit while the Go runtime believes it is comfortably under its own. ## What "soft" means, exactly A hard limit would mean the runtime refuses allocations or aborts when the limit is reached. `GOMEMLIMIT` does neither. It is an aim, not an enforcement: - The runtime will collect more and more aggressively as memory approaches the limit. - If the **live** set alone exceeds the limit, there is nothing collection can do - that memory is reachable. The program keeps running, over the limit. - To prevent a program from being pinned in continuous, futile collection while trying, the runtime includes a **GC CPU limiter** that caps total garbage-collection CPU at 50%. When that cap binds, the collector effectively gives up on holding the line and lets the limit be exceeded rather than spending the whole machine on marking. That last mechanism is the reason a soft limit degrades rather than deadlocks. A hard limit would turn a memory-hungry moment into a crash; a soft one turns it into extra CPU, and then, when even that is bounded, into extra memory. ## Setting it `GOMEMLIMIT` is an environment variable taking a byte count with an optional unit suffix - `B`, `KiB`, `MiB`, `GiB` or `TiB` - and its default is effectively unlimited. The same value can be set and read from inside the program with `runtime/debug.SetMemoryLimit`, which is useful when the limit has to be derived at startup rather than known in advance. ## The mental model to keep Think of `GOGC` as a *ratio* and `GOMEMLIMIT` as a *ceiling*, with the pacer always obeying whichever is more restrictive at that moment. The ratio keeps the collector proportionate at any scale; the ceiling stops the ratio from writing cheques the machine cannot cash. Because the ceiling is soft, exceeding it is a signal about the live set rather than a fault to be caught - the collector has already done everything collection can do.
- Does setting GOMEMLIMIT stop GOGC from having any effect?No. Both goals are computed every cycle and the pacer takes the smaller one. Well below the limit the GOGC goal is smaller and the limit does nothing; near the limit the limit-derived goal wins. They coexist, and either can be the binding constraint at different points in the same process's life.
- What memory is outside GOMEMLIMIT's accounting?Anything the Go runtime does not manage: allocations made through cgo, mappings the program creates itself, and the binary's own text and data. A process dominated by such memory can sit comfortably under its Go memory limit while its actual footprint is far larger.
- If the live set alone exceeds GOMEMLIMIT, what does the runtime do?It runs over the limit. Live memory is reachable, so no amount of collecting can reclaim it, and the limit is soft - the runtime will not fail allocations or abort. The GC CPU limiter caps collection at 50% of CPU so the program is not trapped collecting forever in the attempt.
GOGC is a rule about how untidy a room may get relative to what is in it; GOMEMLIMIT is the size of the room. The pacer obeys whichever bites first, and when the room is genuinely too small it does not throw anything of yours away.
saying these in an interview costs you the question
- Thinks the runtime aborts or fails allocations at the limit
- Says GOMEMLIMIT caps only the heap
- Believes setting a memory limit disables GOGC
- Assumes cgo allocations count against the limit
- Expects the limit to be honoured no matter how large the live set grows