The Go Runtime: Scheduler, GC, and Memory
Your Go source compiles to machine code, but execution is not bare metal.
A bundled runtime schedules goroutines, manages heap memory, runs the garbage collector, and coordinates syscalls so thousands of lightweight tasks share a small thread pool efficiently.
Runtime Basics collects runnable snippets for runtime.MemStats and GC metrics.
Sibling articles cover tri-color marking, Green Tea GC in Go 1.26, escape analysis, tuning knobs, stack growth, leak patterns, and production workflows.
Summary
- The Go runtime is the layer between your compiled program and the OS. It owns goroutine scheduling, heap allocation, and automatic memory reclamation.
- Insight: Performance surprises in Go services often trace to scheduler behavior, GC pacing, or heap growth - not application logic bugs alone.
- Key Concepts: GPM scheduler, heap allocator, tri-color mark-sweep, write barrier, GOGC, GOMEMLIMIT, STW pause, escape analysis.
- When to Use: Read this mental model before tuning latency, diagnosing memory growth, sizing containers, or interpreting
pprofheap and goroutine profiles. - Limitations/Trade-offs: The runtime hides complexity but cannot fix unbounded allocations, goroutine leaks, or CPU-bound work that ignores
GOMAXPROCSlimits. - Related Topics: Concurrency fundamentals,
pprofprofiling, container memory limits, compiler escape analysis, observability metrics.
Foundations
Picture a running Go process as three cooperating subsystems.
Scheduling decides which goroutine runs on which OS thread.
Allocation hands out heap blocks when values cannot live on the stack.
Garbage collection traces live pointers and frees unreachable heap memory.
Goroutines are not OS threads.
Each goroutine starts with a small stack (on the order of a few KiB) that grows on demand.
The runtime parks goroutines that block on channels, mutexes, or I/O so the underlying thread can run other work.
The GPM model names the pieces:
- G - a goroutine (user code plus stack).
- M - an OS thread that executes Go code or enters syscalls.
- P - a logical processor holding a local run queue and scheduling context.
GOMAXPROCS sets how many Ps exist.
That caps how many goroutines execute Go bytecode in parallel on CPU cores, not how many goroutines you may create.
Heap objects are created when the compiler cannot prove a value's lifetime fits a stack frame.
Returning a pointer to a local variable, storing data in interfaces or maps with unknown lifetime, or capturing variables in closures that outlive the frame commonly force heap allocation.
The allocator organizes memory in size classes inside pages (typically 8 KiB in the Go heap).
The garbage collector is tracing: it walks pointers from roots (globals, stacks, registers) and marks reachable objects.
Unreachable objects are swept and their memory returned to the allocator.
Mechanics & Interactions
The scheduler and GC interact constantly.
When the GC needs to inspect stacks or enforce a stop-the-world (STW) phase, goroutines are paused briefly.
Most marking work runs concurrently with your code via write barriers that track pointer updates while the mark phase proceeds.
The classic algorithm is tri-color mark-sweep:
white = not yet visited (candidate garbage)
gray = visited, children not fully scanned
black = visited, children fully scanned
The runtime maintains the invariant that no black object points to a white object without a gray object in between.
Pacing decides when the next GC cycle starts.
By default GOGC=100 means the heap may grow to roughly 100% of live memory since the last collection before triggering another cycle.
GOMEMLIMIT (Go 1.19+) adds a soft memory cap so the collector paces more aggressively as RSS approaches a limit - critical in Kubernetes pods with hard cgroup ceilings.
Go 1.26 makes Green Tea GC the default collector.
Instead of a per-object work list that jumps randomly through the heap, Green Tea batches scanning by page, improving CPU cache locality and enabling vectorized scan loops on modern x86 hardware.
Opt out at build time with GOEXPERIMENT=nogreenteagc if you need the legacy mark path for comparison.
// Illustrative: inspect runtime state from application code
import "runtime"
func snapshot() {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
_ = ms.HeapAlloc // bytes in use on heap
_ = ms.NumGC // completed GC cycles
_ = ms.PauseTotalNs
}Escape analysis runs at compile time.
When a value's address cannot escape its function, the compiler allocates on the stack - no GC involvement, freed on return.
When it escapes, every allocation becomes GC work.
Advanced Considerations & Applications
Production services instrument the runtime rather than guessing.
Export runtime/metrics or parse MemStats for HeapAlloc, HeapInuse, StackInuse, NumGoroutine, and per-cycle PauseNs.
Pair heap profiles from net/http/pprof with allocation traces to find hot make calls or unintended boxing.
Container sizing should account for stack memory (goroutine count times average stack) plus heap goal driven by GOGC and GOMEMLIMIT.
A service with millions of parked goroutines can use substantial RSS even when heap objects are modest.
| Knob | Strength | Weakness | Best Fit |
|---|---|---|---|
| Default GC (Green Tea) | Lower mark CPU on typical heaps | Some irregular heaps see less benefit | Go 1.26+ services without special GC needs |
GOGC tuning | Trades CPU for memory or vice versa | Mis-tuning raises latency spikes | Batch workers vs latency-sensitive APIs |
GOMEMLIMIT | Respects cgroup caps | Soft limit; OOM still possible under spikes | Kubernetes, serverless, shared hosts |
Manual runtime.GC() | Forces collection for tests | Hurts prod latency if abused | Benchmarks, diagnostic snapshots only |
sync.Pool | Reuses transient buffers | Pool entries can be cleared at GC | Serialization buffers, encode scratch space |
Finalizers (runtime.SetFinalizer) run arbitrary code during GC and are easy to misuse.
Prefer explicit Close() methods and defer for resources.
STW pauses are short in modern Go but still matter for tail latency.
Measure with GODEBUG=gctrace=1 during load tests, not only in dev.
Common Misconceptions
- "Go has no memory management concerns because of GC." - GC reclaims unreachable objects; it does not cap growth from leaks, unbounded caches, or goroutine stacks.
- "More goroutines always mean more parallelism." - Only up to
GOMAXPROCSgoroutines run Go code on CPU at once; excess goroutines queue or park. - "STW means the whole process freezes for seconds." - Modern Go keeps STW phases sub-millisecond in typical services; concurrent marking does the heavy work.
- "Setting GOGC very high eliminates GC overhead." - You trade GC CPU for larger heaps and longer mark phases when collection eventually runs.
- "Escape analysis is only a micro-optimization." - Escapes directly determine allocation rate, which drives GC CPU and cache pressure at scale.
- "Finalizers are a fine substitute for destructors." - Finalizers run late, in unpredictable order, and prolong object lifetime.
FAQs
What does the Go runtime actually include?
Goroutine scheduler, memory allocator, garbage collector, stack management, reflection support, panic/defer machinery, and interfaces to the OS (syscalls, signals, timers).
Your binary links it automatically - there is no separate JVM-style install.
How is the GPM scheduler different from OS threads?
OS threads are heavy (MB-scale stacks, kernel scheduling).
Goroutines are cheap (KiB-scale stacks, user-space scheduling).
Ms execute goroutines; Ps spread work across cores; blocking operations park goroutines without blocking threads unnecessarily.
When does the garbage collector run?
When heap growth crosses the GC trigger derived from GOGC, live heap size, and optionally GOMEMLIMIT.
You can also trigger manually with runtime.GC() for diagnostics.
What is a write barrier?
A small piece of code the compiler inserts when pointers are updated during concurrent marking.
It notifies the GC so it does not miss live objects while the heap mutates under running goroutines.
What changed with Green Tea GC in Go 1.26?
Green Tea became the default collector.
It scans by heap page instead of individual objects on the work list, improving locality and reducing mark-phase CPU for many workloads.
Build with GOEXPERIMENT=nogreenteagc to use the legacy algorithm.
Does GOMAXPROCS affect GC?
Indirectly.
More Ps mean more parallel mark workers during GC, but also more concurrent mutator CPU competing for cache and memory bandwidth.
Match GOMAXPROCS to container CPU limits.
Why do I see heap growth after GC?
Live data remains allocated.
HeapAlloc after GC reflects reachable objects plus allocator fragmentation and unused spans not yet returned to the OS.
HeapIdle and HeapReleased show memory returned to the OS over time.
How do stacks relate to GC memory?
Goroutine stacks are not garbage-collected objects but still consume RSS.
Deep recursion or millions of goroutines increase StackInuse in MemStats independently of heap pressure.
Can I disable the garbage collector?
Not in production builds.
GOGC=off exists for specialized benchmarking but is unsafe for long-running services because the heap grows without bound.
How does escape analysis connect to GC load?
Stack allocations skip the heap entirely.
Every escape forces an allocation the GC must eventually trace and sweep.
High allocation rate is the main driver of GC CPU time.
What should I monitor in production?
process_resident_memory_bytes, go_memstats_heap_alloc_bytes, go_memstats_gc_cpu_fraction, goroutine count, and tail latency during GC phases.
Alert on sustained growth, not single spikes after deploy.
Is the runtime the same on TinyGo or WASM?
TinyGo and wazero hosted builds use different runtimes with distinct GC and scheduling trade-offs.
This section focuses on the standard cmd/compile runtime for Linux/macOS/Windows server binaries.
Related
- Runtime Basics - runnable
MemStatsand goroutine count examples - Garbage Collector: Tri-Color Mark-Sweep & Pacing - mark phase mechanics and pacing math
- Green Tea GC & GOEXPERIMENT Flags (Go 1.26) - default collector and build-time flags
- GOGC, GOMEMLIMIT & Finalizers - production tuning knobs
- Goroutine Stacks & Stack Growth - stack size and copy cost
- Goroutines: Creation Cost & Scheduling Overview - GPM scheduling deep dive
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).