What does Go's GOMEMLIMIT environment variable do, and why is it a soft limit?
answer
- a ceiling the collector aims for
- nothing ever refuses an allocation
- heap plus stacks plus runtime metadata
- bytes, or a KiB/MiB/GiB suffix
- runtime/debug can change it while running
basics
~20 sGOMEMLIMIT 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.
solid answer
~40 s`GOMEMLIMIT` (Go 1.19+) gives the runtime a byte ceiling for all the memory it manages — the Go heap plus goroutine stacks and the runtime's own bookkeeping. It is an input to garbage-collection pacing: the closer total managed memory gets to the ceiling, the sooner and more often a cycle is scheduled, so the collector works harder to stay underneath. It is deliberately *soft*. No allocation ever fails, nothing panics, and no goroutine is blocked because of it. If the live data itself is bigger than the ceiling, the program simply runs above it while collecting almost continuously. You set it as a byte count with an optional suffix (`GOMEMLIMIT=800MiB`), or from code with `debug.SetMemoryLimit`; the default is effectively off (`math.MaxInt64`).
code
go · 8 lines// Give the Go runtime 800 MiB, leaving the rest of the allowance
// for memory the limit does not govern.
prev := debug.SetMemoryLimit(800 << 20)
// A negative argument reads the current limit without changing it.
current := debug.SetMemoryLimit(-1)
_, _ = prev, currentgo deeper
Be ready to state the two facts plainly: it is a byte ceiling for memory the Go runtime manages, and it is soft, so the collector just works harder rather than anything failing. Knowing the environment-variable form and the suffix syntax is enough here.
You are expected to explain the mechanism — it feeds GC pacing, so cycles start sooner as the total approaches the ceiling — and to say precisely what is inside its accounting (heap, stacks, runtime metadata) and what is outside it.
Show that you know what the softness costs you in production: a ceiling that the live data exceeds buys continuous collection instead of safety, so it is not a substitute for bounding how much state the service retains.
Own the framing that this knob converts one failure mode into another rather than removing a failure. Be ready to say who sets the number, where it comes from, and what evidence justifies changing it.
## What GOMEMLIMIT is `GOMEMLIMIT` is an environment variable, introduced in Go 1.19, that tells the Go runtime how much memory it should try to keep itself within. Its counterpart in code is `runtime/debug.SetMemoryLimit`, which takes and returns an `int64` number of bytes. Before it existed, the only knob shaping when the garbage collector ran was `GOGC`, a *relative* one: collect when the heap has grown by some percentage over the live data left after the last cycle. Relative pacing has no idea how much memory the machine or the container actually has. `GOMEMLIMIT` adds an *absolute* input, expressed in bytes. ## What it counts The ceiling covers **the memory the Go runtime manages**: - the Go heap (both live objects and garbage not yet collected), - goroutine stacks, - the runtime's own metadata — span and page bookkeeping, GC work structures, and so on. It explicitly excludes memory the runtime does not manage: the mappings of the binary itself, anything allocated by C code reached through cgo, memory-mapped files, and memory the operating system holds on the program's behalf. That distinction matters the moment you compare the number to a container's memory limit, because the kernel counts everything and the runtime counts only its own share. ## Why "soft" The word is doing real work. A hard limit is one an allocator enforces: cross it and the allocation is refused, an exception is raised, or the process dies. Go's memory limit does none of that. It is a **target the pacer aims for**, nothing more: - allocations never fail (Go's allocation expressions have no error result at all); - no goroutine is parked waiting for memory; - the runtime does not terminate the program. What actually changes is the *schedule* of the collector. As total managed memory rises toward the ceiling, cycles start earlier and closer together so that more garbage is reclaimed per unit of time. If garbage is available, that keeps you under the number. If it is not — because the data is genuinely live — the runtime keeps allocating and goes over the ceiling anyway. To stop the pathological case where the collector consumes the whole machine trying to reach an impossible target, the runtime also has a GC CPU limiter that caps collection at roughly half the available CPU; the program is allowed to exceed the memory limit rather than grind to a complete halt. So the honest summary is: **GOMEMLIMIT changes how hard the collector tries, not what happens when trying fails.** ## Setting it The environment form takes a byte count with an optional unit suffix — `B`, `KiB`, `MiB`, `GiB`, `TiB`: ``` GOMEMLIMIT=900MiB ``` A bare number means bytes. The default is `math.MaxInt64`, which is effectively no limit, so an unconfigured program behaves exactly as it did before Go 1.19. From code, `debug.SetMemoryLimit(n)` sets the ceiling and returns the previous one; passing a negative value queries the current setting without changing it. Setting it from code is what you want when the budget must be derived at startup from something the process learns about its environment, rather than baked into the binary. ## What it is good for The motivating case is a process with a fixed memory allowance — typically a container — where running out of memory means the process is killed outright with no Go-level warning: no panic, no stack trace, just a dead process. A relative GC trigger cannot see that cliff. Giving the runtime the number lets it collect more aggressively as it approaches, converting what would have been a sudden death into extra GC work. It is also what makes it safe to relax the growth trigger: a service can be told *use the room you have been given, but no more*, so that a heap far below its allowance stops being collected pointlessly often. ## Where people go wrong The two mistakes to avoid are treating it as a hard cap (it is not — the process can and will exceed it), and setting it equal to the container's memory limit (it governs only part of the process's footprint, so you must leave headroom for the rest). Both follow directly from the two facts above: it is soft, and it counts runtime-managed memory only.
- What happens if a program's live data is genuinely larger than GOMEMLIMIT?It keeps running above the ceiling. The collector cannot reclaim reachable objects, so cycles start back to back and GC CPU climbs, while allocation continues to succeed. The runtime's GC CPU limiter caps collection at about half the CPU so the program makes some progress instead of freezing. Nothing in Go terminates the process; only the operating system can.
- How do you set or read the limit from inside a running Go program?`debug.SetMemoryLimit(n)` in `runtime/debug` sets the ceiling to `n` bytes and returns the previous value. Passing a negative number is defined as a query: it returns the current limit and changes nothing. Setting it from code is the right choice when the budget has to be computed at startup from a value the process is given, rather than fixed at build time.
- What is the default value, and what paces the collector when no limit is set?The default is `math.MaxInt64` — a ceiling so large it never binds, which is why programs behave as they did before Go 1.19 unless you configure it. With no effective ceiling, cycles are triggered purely by heap growth relative to the live data surviving the previous cycle.
GOMEMLIMIT is a household budget, not a card that gets declined. Approaching it makes you economise much harder, but nothing at the till refuses the transaction.
saying these in an interview costs you the question
- Calls GOMEMLIMIT a hard cap that prevents the process being killed
- Says allocations start failing or panicking once the ceiling is crossed
- Believes it bounds the whole process footprint, including cgo allocations
- Thinks it stops goroutines until the collector frees memory
- Assumes setting it equal to the container's memory limit is safe