How to Tune the Go Garbage Collector (GOGC, GOMEMLIMIT)

Set GOGC to adjust GC frequency or GOMEMLIMIT to cap memory usage in Go applications.

When defaults stop working

Your Go service handles thousands of requests per second. The CPU stays flat. The response times are fine. Then the memory usage climbs past two gigabytes and refuses to come down. The next thing you know, the container orchestrator kills the pod for exceeding its limit, or the server starts dropping requests because the garbage collector is spending more time cleaning up than processing work. You assume you have a memory leak. You spend three days hunting for unclosed database connections and forgotten channel buffers. The problem is not your code. The problem is the garbage collector running on default settings that do not match your workload.

How the knobs actually work

Go manages memory automatically. You allocate objects, and the runtime reclaims them when they are no longer reachable. The runtime decides when to run the cleanup cycle. By default, it uses a heuristic that works for most applications. Some applications need different behavior. High-throughput batch jobs prefer fewer pauses and higher memory peaks. Latency-sensitive APIs prefer lower memory peaks and more frequent, shorter pauses.

You control this behavior with two environment variables. GOGC sets the percentage of heap growth that triggers a garbage collection cycle. GOMEMLIMIT sets a hard ceiling on how much memory the process is allowed to use. Think of GOGC as a thermostat. When the heap grows by a certain percentage since the last collection, the runtime kicks in. Think of GOMEMLIMIT as a physical wall. The runtime will never allocate past that line, even if it means running the collector more aggressively or panicking.

Tune the thermostat to match your workload. Do not fight the wall.

The minimal setup

Here is the simplest way to change the defaults before your program starts.

# Set GC to trigger when heap doubles since last cycle
export GOGC=200
# Cap total process memory at 4 gigabytes
export GOMEMLIMIT=4GiB
# Run the application with the new limits
go run main.go

The environment variables are read during process startup. The runtime stores them internally and uses them to configure the garbage collector before any user code runs. This approach is the standard convention for containerized deployments. Kubernetes and Docker compose files pass these variables directly to the process, keeping configuration outside the binary.

What happens under the hood

When your program allocates memory, the runtime tracks the size of the live heap. The default GOGC value is 100. This means the runtime waits until the heap has grown by 100 percent since the last collection before starting a new cycle. If your last collection left 500 megabytes of live data, the runtime will allow allocations until the heap hits 1 gigabyte. Once it crosses that threshold, the mark-sweep phase begins.

Setting GOGC=200 changes the math. The runtime now waits until the heap grows by 200 percent. That same 500 megabyte baseline allows allocations up to 1.5 gigabytes before the collector runs. Higher values mean fewer collections, higher peak memory, and longer pauses when the cycle finally triggers. Lower values mean more frequent collections, lower peak memory, and shorter pauses.

GOMEMLIMIT operates differently. It does not use percentages. It uses absolute bytes. When you set GOMEMLIMIT=4GiB, the runtime treats that number as a hard boundary. If the heap approaches the limit, the runtime forces a collection regardless of the GOGC percentage. If the collection cannot free enough memory to stay under the limit, the runtime panics. This prevents the operating system from killing your process with an out-of-memory signal. The runtime handles the failure gracefully instead.

The two settings work together. GOGC drives the normal rhythm of collections. GOMEMLIMIT acts as an emergency brake. If your application normally runs at 2 gigabytes but experiences a traffic spike, GOGC will trigger collections at the configured percentage. If the spike pushes memory toward the GOMEMLIMIT ceiling, the runtime will force collections more aggressively to stay under the cap.

Real-world configuration

Production services often need to adjust these limits at runtime based on deployment constraints. You can change them programmatically using the runtime/debug package. This approach is useful when your application reads configuration from a database or a remote config service after startup.

package main

import (
	"fmt"
	"runtime"
	"runtime/debug"
)

// main starts a simple server and configures memory limits
func main() {
	// Set a hard memory ceiling of 2 gigabytes
	debug.SetMemoryLimit(2 * 1024 * 1024 * 1024)
	// Disable automatic GC percentage tracking
	debug.SetGCPercent(-1)
	// Print the current memory statistics to verify
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("Heap limit: %d bytes\n", m.HeapLimit)
}

The debug.SetMemoryLimit function accepts a byte count. Pass -1 to remove the limit entirely. The debug.SetGCPercent function accepts an integer. Pass -1 to disable the percentage-based trigger completely. When you disable the percentage trigger, the runtime relies entirely on the memory limit to decide when to collect. This combination gives you precise control over pause frequency and peak usage.

A quick convention note: the Go community prefers environment variables for static limits and runtime/debug for dynamic tuning. Mixing both in the same deployment creates confusion. Pick one source of truth and stick to it.

Where tuning goes wrong

Tuning the garbage collector introduces new failure modes. Setting GOGC too high delays collections until the heap grows massive. The next collection cycle will take longer because it has more pointers to scan. You will see longer stop-the-world pauses and higher tail latencies. Setting GOGC to 0 or off disables automatic collections entirely. Your process will consume memory until it hits the operating system limit or exhausts swap space.

Setting GOMEMLIMIT too low causes thrashing. The runtime triggers a collection, frees a small amount of memory, hits the limit again, and triggers another collection. The CPU spends most of its time running the collector instead of your application. If the limit is so low that even a single allocation would exceed it, the runtime panics with runtime: out of memory. This panic is different from a standard Go panic. It cannot be recovered with recover. The process terminates immediately.

Another common mistake is confusing heap memory with total process memory. GOMEMLIMIT only caps the Go heap. It does not account for memory used by the operating system, shared libraries, or memory mapped files. If your application uses large mmap regions or caches files on disk, those allocations bypass the Go heap and ignore the limit. The container orchestrator will still kill the pod if the total RSS exceeds the cgroup limit.

Watch the heap, not the process. The GC only knows about Go allocations.

Choosing the right settings

Use GOGC=100 when you are running a standard web service or API and want the runtime to balance pause duration and memory usage automatically. Use GOGC=200 or higher when you are running batch processing jobs, data pipelines, or workloads that allocate large temporary buffers and can tolerate longer pauses in exchange for lower CPU overhead. Use GOGC=50 or lower when you are running latency-sensitive request handlers that cannot afford long stop-the-world pauses and can handle more frequent, shorter collection cycles. Use GOMEMLIMIT when you are deploying to containers with strict memory quotas and need to prevent the operating system from killing your process with an OOM signal. Use debug.SetGCPercent(-1) combined with GOMEMLIMIT when you want deterministic collection behavior that ignores heap growth percentages and only reacts to absolute memory boundaries.

Defaults are starting points. Measure your pauses, then adjust.

Where to go next