What does runtime/debug.FreeOSMemory do that an ordinary Go garbage collection cycle does not?
answer
- the runtime already does this, slowly
- it collects first, then hands memory back
- immediate and complete instead of gradual
- returned pages must be faulted back in
- a phase boundary, never a ticker
basics
~20 sFreeOSMemory forces a garbage collection and then tries to hand as much free heap memory back to the operating system as it can, at once. An ordinary cycle frees objects but leaves a background task to return pages gradually.
solid answer
~50 sAn ordinary GC cycle reclaims dead objects into the runtime's idle spans; it does not unmap anything. The runtime returns that idle memory to the OS gradually, in a background task, on its own schedule. `debug.FreeOSMemory` collapses both steps into one synchronous call: run a collection now, then attempt to return as much of the resulting free memory to the operating system as possible. The cost is real. You pay for a full collection at a moment you chose rather than the pacer chose, and you throw away exactly the mapped-and-empty pages the runtime was keeping so it could grow again cheaply — so the next growth phase faults them all back in. That makes it defensible once at a phase boundary, such as after a large one-off batch or before a long idle period, and a poor idea on a ticker.
code
go · 6 linesfunc (j *ImportJob) Finish() {
j.rows = nil // drop the last reference to the bulk data
// collect now, then hand the freed pages back now
debug.FreeOSMemory()
}go deeper
Remember that debug.FreeOSMemory lives in runtime/debug and that it both collects garbage and asks the operating system to take memory back. Know that ordinary application code does not need it.
Explain that the runtime already returns memory gradually in a background task and that this call only makes the return immediate and as complete as possible. Be able to state what it costs.
Show judgment about placement: once at a phase boundary after a large batch, or before a long idle period, or as a one-shot experiment on a canary. Explain why a ticker trades throughput for a prettier graph.
Treat a standing FreeOSMemory call in a service as a signal rather than a fix. Ask what memory shape made someone add it, and whether the real answer is lowering the allocation peak.
## What the call actually is `FreeOSMemory` lives in the `runtime/debug` package. Its documented behaviour is short and worth quoting in spirit: it forces a garbage collection followed by an attempt to return as much memory to the operating system as possible — and it notes that even if it is never called, the runtime gradually returns memory to the OS in a background task. That parenthetical is the whole point of the question. Nothing about `FreeOSMemory` is unique except **timing and completeness**. ## The two steps a normal cycle leaves undone A normal collection cycle ends with dead objects reclaimed. The memory those objects occupied becomes idle spans in the runtime's page allocator. Idle spans are still mapped, still resident, and still counted by the kernel against your process. They exist so the next allocation burst can be satisfied without a syscall, which is a good trade for a service whose memory use oscillates. Separately, a background scavenger walks idle memory and returns some of it to the operating system over time. It is deliberately gradual, because returning a page you are about to need again means paying a page fault to get it back. `FreeOSMemory` short-circuits both: collect now, and release now, as completely as it can. ## What it costs 1. **A collection you did not schedule.** You get GC work — including its stop-the-world phases — at a moment of your choosing rather than one chosen by the runtime's own accounting of allocation rate. 2. **The arena you just gave away.** The retained pages were an asset: free memory the runtime could reuse without touching the kernel. After a release, the next growth phase must request address space again and fault pages in one at a time. On a service that keeps allocating, this shows up as a latency bump and higher system CPU immediately after the call. 3. **No effect on anything reachable.** If a live object graph is holding memory, `FreeOSMemory` cannot help. It releases *free* memory. Calling it and seeing no change in RSS is itself a useful diagnostic signal: it says the memory is either live, or outside the Go heap. ## Where it is genuinely appropriate - **After a one-off bulk phase.** A CLI or job that loaded a very large dataset, finished with it, and now enters a long low-memory phase. The peak is over and will not return; releasing is a pure win. - **Before a long idle period.** A process that will sit mostly unused (a rarely used sidecar, a scheduled worker between runs) has no upcoming growth to protect, so the retained arena buys nothing. - **As a one-shot diagnostic.** On a canary instance, calling it once and watching whether RSS falls distinguishes "the runtime is retaining free pages" from "the memory is actually live or off-heap". That single experiment is often the fastest way to close out a memory question. ## Where it is not Calling it on a timer is the classic misuse. It converts a stable arena into a permanent cycle of release-and-refault, adds recurring GC pauses that the pacer did not ask for, and makes the memory graph prettier while making the service slower. When you find that pattern in a codebase, the question to ask is not "is the interval tuned right" but "what memory shape made someone add this" — the honest answer is usually a peak that should be lowered, or a large allocation that should be reused rather than rebuilt. It is also not a leak fix. Memory held by reachable objects is untouched by it. If someone added a periodic `FreeOSMemory` and RSS still climbs, the diagnosis was wrong from the start and the live heap is where to look. ## The mental model Think of the runtime as holding a working float of memory. A normal collection returns cash to the float. The background scavenger deposits some of the float back at the bank, slowly, because withdrawals are expensive. `FreeOSMemory` empties the float into the bank immediately — correct when you are closing for the night, wasteful when you will need to make change again in thirty seconds.
- You call debug.FreeOSMemory and RSS does not move. What have you learned?That the memory is not free heap memory. Either it is still reachable — a live object graph is holding it, which is a leak or an oversized cache — or it is outside the Go heap entirely: goroutine stacks, cgo allocations, explicit mappings. Either way the next step is not another release attempt; it is `runtime.MemStats` and a heap profile to decide which of those two it is.
- Why is calling debug.FreeOSMemory on a ticker a bad default?It repeatedly discards the mapped free memory the runtime keeps precisely so it can grow again without syscalls, so every subsequent allocation burst pays page faults to reacquire it. You also take a full collection on your schedule rather than the runtime's. The result is a flatter memory graph and a slower service, which is rarely the trade anyone intended.
- Does debug.FreeOSMemory help a service whose live heap keeps growing?No. It releases free memory, and a growing live heap is by definition not free — something is still reachable. The call will run a collection, find little to reclaim, release almost nothing, and cost you a pause. A periodic FreeOSMemory in a service with a climbing live heap is a sign the original diagnosis was wrong.
The runtime keeps a working float of cash so it does not have to visit the bank for every transaction. FreeOSMemory empties the float into the bank right now: sensible at closing time, wasteful if you need change again in a minute.
saying these in an interview costs you the question
- Calls it on a timer to keep the memory graph flat
- Presents it as a fix for a memory leak
- Thinks it only releases memory without collecting
- Does not know the runtime already releases gradually
- Ignores the page faults the next growth phase pays