Golang garbage collection: GC, heap, GOGC, runtime.GC, and tuning

Tech reviewed: Deepak Prasad
Golang garbage collection: GC, heap, GOGC, runtime.GC, and tuning

Golang garbage collection (and related searches such as go garbage collection, garbage collection in golang, garbage collection in go, golang garbage collector, go garbage collector, and garbage collector golang) refers to the runtime automatically freeing unreachable heap objects. The golang gc uses a mostly concurrent algorithm with tri-color marking; golang force garbage collection is possible via runtime.GC() but is not a substitute for fixing allocation hot spots. This guide summarizes the heap, tuning GOGC and GOMEMLIMIT, observability (runtime.ReadMemStats, GODEBUG=gctrace=1, pprof), and when manual GC is appropriate.

Tested with Go 1.24 on Linux.


Golang Garbage Collection Quick Cheat Sheet

Command / Concept Description
runtime.GC() Manually trigger garbage collection
runtime.ReadMemStats(&m) Get memory and GC statistics
m.HeapAlloc Currently allocated heap memory
m.NumGC Number of GC cycles executed
GOGC=100 Default GC target percentage
GOGC=50 Run GC more frequently (lower memory usage)
GOGC=200 Run GC less frequently (better performance)
debug.SetGCPercent(n) Change GC percentage at runtime
GODEBUG=gctrace=1 Enable detailed GC logs
GODEBUG=gctrace=1 go run main.go Run program with GC tracing
pprof Analyze memory and GC behavior
HeapIdle Unused heap memory
HeapReleased Memory returned to OS
Stop-the-world (STW) Brief pause during GC phases
Concurrent GC GC runs alongside application
Tri-color marking Algorithm used by Go GC

Heap in golang (HeapAlloc, stack vs GC heap)

Heap in golang usually means memory managed by the allocator and scanned by the golang gc: pointers live here, and runtime.MemStats.HeapAlloc reports bytes currently allocated on the heap. Stack frames for goroutines are separate; escape analysis decides whether a value stays on the stack or moves to the heap. When you read MemStats after allocations, watch HeapAlloc (live bytes), HeapIdle (cached free spans), HeapReleased (returned to the OS), and NumGC (completed cycles).


What is Garbage Collection in Golang

Garbage collection in Golang is an automatic memory management mechanism that identifies and frees unused memory during program execution. It helps developers avoid manual memory handling while improving application stability and performance.

How garbage collector works in Go

Go uses a concurrent mark-and-sweep garbage collector that runs alongside your application. It identifies reachable objects, marks them, and removes unused ones with minimal pause time.

Here is a simple golang example where the string object "hello, world" is allocated memory when the variable s is declared. As long as the variable s is in scope, the string object is considered reachable and will not be garbage collected. Once the variable s goes out of scope, the string object is no longer reachable and will be eligible for garbage collection.

go
package main

import "fmt"

func main() {
    // Allocate memory for a new string object
    s := "hello, world"

    // The variable s is still in scope, so the string object it references is considered reachable
    fmt.Println(s)

    // Once the variable s goes out of scope, the string object it references is no longer reachable
    // and will be eligible for garbage collection
}
Output

Does Go have automatic memory management

Yes, Go provides automatic memory management. Developers do not need to manually free memory, as the garbage collector handles allocation cleanup automatically.

Why GC is important in Go applications

Garbage collection prevents memory leaks, reduces development complexity, and ensures efficient memory usage in long-running applications like APIs and microservices.


How Golang Garbage Collector Works Internally

Mark and sweep algorithm explained simply

The GC works in two main phases:

  • Mark phase: Identifies objects that are still reachable (in use)
  • Sweep phase: Frees memory of objects that are no longer reachable

Tricolor marking (white, grey, black)

Go uses a tricolor marking algorithm:

  • White: Objects not yet processed (potential garbage)
  • Grey: Objects reachable but not fully scanned
  • Black: Objects fully processed and confirmed in use

This approach ensures safe and efficient memory cleanup.

What is GC cycle in Go

A GC cycle includes marking live objects, sweeping unused memory, and preparing the heap for future allocations. These cycles run periodically based on memory usage.


Force Garbage Collection in Golang

Use runtime.GC() to trigger GC manually

You can force garbage collection using the runtime package.

go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    runtime.GC()
    fmt.Println("Garbage collection triggered")
}
Output

When to force GC in real applications

Manual GC is useful in:

  • Memory-sensitive applications
  • Batch processing jobs
  • Debugging and testing scenarios

Avoid frequent manual GC in production as it may impact performance.

golang force garbage collection example

In this example we use runtime.GC to manually trigger the garbage collector:

go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    // Allocate some memory for the program to use
    s := make([]string, 0, 100000)
    for i := 0; i < 100000; i++ {
        s = append(s, "hello, world")
    }

    // Print the initial memory usage
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Println("Initial HeapAlloc: ", m.HeapAlloc)

    // Trigger the garbage collector
    runtime.GC()

    // Print the memory usage after the garbage collector has run
    runtime.ReadMemStats(&m)
    fmt.Println("After GC HeapAlloc: ", m.HeapAlloc)

    // Release the memory
    s = nil
    // Trigger the garbage collector
    runtime.GC()
    // Print the memory usage after the garbage collector has run
    runtime.ReadMemStats(&m)
    fmt.Println("After release HeapAlloc: ", m.HeapAlloc)
}
Output

This program uses the runtime package to manually trigger the garbage collector and then prints the heap allocation before and after the garbage collection.

Output:

text
# go run main.go 
Initial HeapAlloc:  1654512
After GC HeapAlloc:  37872
After release HeapAlloc:  37872

GC Performance and Pause Time

What is GC pause time in Golang

GC pause time refers to short moments when the application is paused to perform critical operations like root scanning. Although Go uses a concurrent garbage collector, small pauses (microseconds to milliseconds) still occur.

go
import "runtime"

var m runtime.MemStats
runtime.ReadMemStats(&m)

fmt.Println("Last GC Pause:", m.PauseNs[(m.NumGC+255)%256])

Stop-the-world behavior explained

"Stop-the-world" (STW) means the application is temporarily paused while the GC performs essential operations. In Go, STW phases are minimal and occur only during specific steps like marking roots, ensuring low-latency behavior compared to older GC models.

go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    // Allocate large memory to trigger GC pressure
    data := make([][]byte, 0)

    for i := 0; i < 50; i++ {
        data = append(data, make([]byte, 10*1024*1024)) // 10MB each
    }

    // Measure GC pause
    start := time.Now()
    runtime.GC()
    duration := time.Since(start)

    fmt.Println("GC pause duration:", duration)
}
Output

Output:

text
GC pause duration: 154.197µs

This output shows that the garbage collection completed in around 154 microseconds, which includes a very small Stop-The-World (STW) pause along with concurrent GC work.

In Golang, STW pauses are extremely short and usually not noticeable in real applications. Most of the garbage collection runs concurrently, and only small parts such as root scanning require a brief pause.

This is why Go is well-suited for low-latency and high-performance applications, as GC rarely introduces visible delays.

How GC impacts latency and throughput

Garbage collection directly affects performance:

  • High GC frequency → increased CPU usage
  • Large heap → longer GC cycles
  • Frequent allocations → more GC pressure

You can observe GC behavior using:

bash
GODEBUG=gctrace=1 go run main.go

Example:

bash
GOGC=50 go run main.go

Lower GOGC → more frequent GC → lower memory usage
Higher GOGC → less frequent GC → better performance


Monitor and Debug GC in Golang

Use runtime.ReadMemStats for GC stats

Use runtime.ReadMemStats to observe memory usage before and after allocations.

go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    var m runtime.MemStats

    // Print statistics before the slice is released
    runtime.ReadMemStats(&m)
    fmt.Println("HeapAlloc: ", m.HeapAlloc)
    fmt.Println("HeapIdle: ", m.HeapIdle)
    fmt.Println("HeapReleased: ", m.HeapReleased)
    fmt.Println("NumGC: ", m.NumGC)
    fmt.Println("-----------")

    // Allocate some memory for the program to use
    s := make([]string, 0, 100000)
    for i := 0; i < 100000; i++ {
        s = append(s, "hello, world")
    }

    // Print statistics after the slice is released
    runtime.ReadMemStats(&m)
    fmt.Println("HeapAlloc: ", m.HeapAlloc)
    fmt.Println("HeapIdle: ", m.HeapIdle)
    fmt.Println("HeapReleased: ", m.HeapReleased)
    fmt.Println("NumGC: ", m.NumGC)
    fmt.Println("-----------")
    // Release the memory
    s = nil

    // Manually trigger garbage collector
    runtime.GC()
    runtime.ReadMemStats(&m)
    fmt.Println("HeapAlloc: ", m.HeapAlloc)
    fmt.Println("HeapIdle: ", m.HeapIdle)
    fmt.Println("HeapReleased: ", m.HeapReleased)
    fmt.Println("NumGC: ", m.NumGC)
    fmt.Println("-----------")
}
Output

Key metrics:

  • HeapAlloc → memory currently in use
  • HeapIdle → unused heap memory
  • HeapReleased → memory returned to OS
  • NumGC → number of GC cycles

Use this to detect memory leaks and GC inefficiencies.

text
# go run main.go 
HeapAlloc:  48048
HeapIdle:  3719168
HeapReleased:  3686400
NumGC:  0
-----------
HeapAlloc:  1654104
HeapIdle:  2072576
HeapReleased:  2072576
NumGC:  0
-----------
HeapAlloc:  37648
HeapIdle:  3710976
HeapReleased:  2039808
NumGC:  1
-----------

We can also use the statistics to detect potential memory leaks and performance issues, also we can use HeapAlloc and HeapReleased to check if our program is releasing memory correctly.

Enable GC logs using GODEBUG=gctrace=1

The GODEBUG environment variable enables detailed GC logs for debugging.

go
package main

import (
	"fmt"
	"runtime"
	"time"
)

func printStats() {
	var mem runtime.MemStats
	runtime.ReadMemStats(&mem)

	fmt.Println("Alloc:", mem.Alloc)
	fmt.Println("TotalAlloc:", mem.TotalAlloc)
	fmt.Println("HeapAlloc:", mem.HeapAlloc)
	fmt.Println("NumGC:", mem.NumGC)
	fmt.Println("-----")
}

func main() {
	printStats()

	// Allocate memory
	for i := 0; i < 10; i++ {
		_ = make([]byte, 100000000) // 100MB allocation
	}

	printStats()

	// Force garbage collection
	runtime.GC()

	printStats()

	// Allow GC to run naturally over time
	for i := 0; i < 5; i++ {
		_ = make([]byte, 100000000)
		time.Sleep(2 * time.Second)
	}

	printStats()
}
Output

We use for loop to obtain large amounts of memory in order to trigger the use of garbage collector.

Output:

text
# go run main.go 
Alloc: 77832
TotalAlloc: 77832
HeapAlloc: 77832
NumGC: 0
-----
Alloc: 100091840
TotalAlloc: 1000167504
HeapAlloc: 100091840
NumGC: 10
-----
Alloc: 84680
TotalAlloc: 1000168312
HeapAlloc: 84680
NumGC: 11
^Csignal: interrupt

So, the output presents information about properties related to the memory used by the main.go program. If we want to get an even more detailed output, we can execute the same tool using GODEBUG, as shown here:

bash
GODEBUG=gctrace=1 go run main.go

Output:

text
gc 1 @0.009s 2%: 0.022+0.49+0.17 ms clock, 0.091+0.072/0.35/0.66+0.71 ms cpu, 3->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P
gc 2 @0.017s 3%: 0.038+1.0+0.008 ms clock, 0.15+0.12/0.94/0.33+0.034 ms cpu, 3->3->1 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P
gc 3 @0.019s 4%: 0.043+1.8+0.019 ms clock, 0.17+0.32/1.3/0+0.079 ms cpu, 3->4->1 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P
...
gc 9 @0.032s 8%: 0.030+0.85+0.021 ms clock, 0.12+0.64/0.70/0.008+0.085 ms cpu, 3->4->2 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P
gc 10 @0.034s 8%: 0.041+1.0+0.007 ms clock, 0.16+0.81/0.93/0.24+0.029 ms cpu, 3->4->2 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P

This table helps interpret GODEBUG=gctrace=1 output and understand how Go garbage collection impacts memory usage and performance.

Field Meaning
gc 1 GC cycle number
@0.009s Time since program start when GC occurred
2% Percentage of CPU time spent in GC
0.022+0.49+0.17 ms GC phases: stop-the-world + mark + sweep
3->4->0 MB Heap size (before → after → live memory)
4 MB goal Target heap size for next GC
4 P Number of logical processors used

From this output, we can see how Go garbage collection behaves in real time. GC runs very frequently because the program continuously allocates memory. The heap size grows (for example 3->4->2 MB) and is reduced after each cycle, showing that unused memory is being reclaimed.

GC pause times remain very low (in milliseconds), which indicates that most of the work is done concurrently, with only very small stop-the-world pauses. CPU usage by GC also stays within a small percentage range (around 1–8%), meaning GC overhead is controlled.

In terms of performance, frequent GC cycles help keep memory usage low but can increase CPU usage slightly. On the other hand, allowing a larger heap reduces GC frequency but may take longer to clean up. Go balances this well by keeping pause times minimal and latency stable.

GC trace logs are useful when you want to understand memory behavior in detail, such as debugging memory leaks, analyzing GC frequency and pause time, optimizing high-memory workloads, or tuning garbage collection using variables like GOGC and GOMEMLIMIT.

Analyze memory using pprof

pprof is a built-in Golang profiling tool used to analyze memory usage, CPU performance, and garbage collection behavior. It is widely used for debugging memory leaks and understanding GC pressure in Go applications.

go
package main

import (
    "log"
    "net/http"
    _ "net/http/pprof"
    "time"
)

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // Simulate some work
    for {
        work()
        time.Sleep(1 * time.Second)
    }
}

func work() {
    _ = make([]byte, 10*1024*1024) // Allocating memory
}
Output

In this example:

  • net/http/pprof exposes profiling endpoints
  • Server runs at localhost:6060
  • Memory allocation simulates GC activity

Access memory profile:
http://localhost:6060/debug/pprof/heap

Learn more about Go HTTP servers:

Together, these tools provide complete visibility into Golang garbage collection behavior.


GC Tuning and Optimization

Golang provides multiple ways to tune garbage collection behavior to balance memory usage, CPU overhead, and application performance. Proper GC tuning is essential for high-throughput services, APIs, and containerized workloads.

golang garbage collection tuning using GOGC

The GOGC environment variable controls when garbage collection runs based on heap growth percentage.

bash
GOGC=100 go run main.go
  • Default is 100 → GC runs when heap grows by 100%
  • Lower value (e.g., 50) → more frequent GC → lower memory usage
  • Higher value (e.g., 200) → less frequent GC → better CPU performance

Use lower values for memory-constrained systems and higher values for CPU-optimized workloads.

Modern GC Tuning: GOGC vs GOMEMLIMIT

In modern Go applications, especially in containerized environments like Kubernetes, controlling memory usage is critical. While GOGC adjusts GC frequency based on heap growth, GOMEMLIMIT (introduced in Go 1.19) defines a hard memory limit for the runtime.

  • GOGC (default 100)
    Controls GC frequency based on heap growth

  • GOMEMLIMIT
    Triggers GC when memory usage approaches a defined limit

bash
GOGC=100 GOMEMLIMIT=512MiB go run main.go

This ensures GC runs efficiently while preventing excessive memory usage.

When to use GOMEMLIMIT

  • Kubernetes / container environments
  • Memory-limited systems
  • Multi-tenant applications

Use GOGC for performance tuning and GOMEMLIMIT for memory safety. Combining both provides optimal results.

How to reduce memory usage and GC overhead

Efficient memory usage reduces GC pressure and improves performance.

  • Minimize unnecessary allocations
  • Reuse objects instead of creating new ones
  • Avoid large temporary data structures
  • Use efficient data types and structures

Example using object reuse:

go
import "sync"

var pool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

Reduce allocation rate (important for GC performance)

High allocation rates increase GC frequency and CPU usage.

  • Avoid repeated string concatenation
  • Use buffers instead of creating new slices
  • Prefer pre-allocated memory where possible

Example:

go
buf := make([]byte, 0, 1024)
buf = append(buf, data...)

Best practices for high-performance apps

  • Monitor GC metrics regularly using runtime.ReadMemStats
  • Use GODEBUG=gctrace=1 for debugging
  • Tune GOGC based on workload behavior
  • Avoid excessive manual GC calls
  • Profile memory using pprof

GC tuning strategy (practical approach)

  • Start with default settings (GOGC=100)
  • Monitor memory and GC behavior
  • Adjust GOGC gradually based on performance needs
  • Use GOMEMLIMIT in containerized environments
  • Validate changes using load testing

This approach ensures stable performance without over-tuning.


Disable or Control GC Behavior

golang disable garbage collection (debug.SetGCPercent)

In some scenarios like benchmarking or testing memory behavior, you may want to temporarily disable garbage collection.

You can disable GC by setting the GC percentage to -1.

go
package main

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

func main() {
    // Disable GC
    debug.SetGCPercent(-1)

    var m runtime.MemStats

    for i := 0; i < 5; i++ {
        _ = make([]byte, 50*1024*1024) // Allocate 50 MB
        runtime.ReadMemStats(&m)
        fmt.Printf("Iteration %d: HeapAlloc = %d MB, NumGC = %d\n",
            i, m.HeapAlloc/1024/1024, m.NumGC)
    }
}
Output

Output (example):

text
Iteration 0: HeapAlloc = 50 MB, NumGC = 0
Iteration 1: HeapAlloc = 100 MB, NumGC = 0
Iteration 2: HeapAlloc = 150 MB, NumGC = 0
Iteration 3: HeapAlloc = 200 MB, NumGC = 0
Iteration 4: HeapAlloc = 250 MB, NumGC = 0

In this example, memory keeps increasing and NumGC remains 0, which confirms that garbage collection is disabled.

WARNING
Disabling GC is not recommended in production, as it can quickly lead to high memory usage or out-of-memory errors. It should only be used for debugging, benchmarking, or controlled experiments.

When disabling GC is useful (edge cases)

  • Short-lived programs
  • Benchmarking scenarios
  • Controlled memory environments

This is rarely needed in production applications.

Risks of disabling GC

  • Memory usage can grow uncontrollably
  • Increased risk of crashes due to OOM
  • Not suitable for long-running services

Always re-enable GC after testing or special use cases.


Summary

The go garbage collector balances CPU, latency, and memory: it is mostly concurrent with tri-color marking, uses the heap metrics above for observability, and defaults (GOGC, optional GOMEMLIMIT) that suit most services. Reach for runtime.GC() or debug.SetGCPercent(-1) only in controlled tests—not as a fix for leaks or spikes. Combine ReadMemStats, gctrace, and heap profiles when golang garbage collection tuning is actually needed.

In most cases, Go’s default GC behavior works well, but performance-critical applications benefit from proper tuning, profiling, and minimizing unnecessary allocations. Tools like runtime.ReadMemStats, GODEBUG, and pprof provide the data to decide whether to change GOGC or allocation patterns.


References


Frequently Asked Questions

1. What is garbage collection in golang?

The Go runtime automatically reclaims memory for values that become unreachable so you rarely call free manually; collection is mostly concurrent with short stop-the-world phases.

2. How do I golang force garbage collection?

Call runtime.GC() to run a synchronous collection cycle; use sparingly in production because it costs CPU and the runtime already schedules collections based on heap growth and GOGC.

3. What is GOGC in golang?

GOGC sets the heap growth percentage relative to live data before the next GC cycle (default 100); lower values collect more often and use less memory, higher values collect less often.

4. Does golang have stop-the-world GC?

Yes briefly: the collector is concurrent for much of its work, but some phases still need small STW pauses; see GODEBUG=gctrace=1 for timings.

5. Can we disable garbage collection in go?

You can set debug.SetGCPercent(-1) to effectively disable automatic cycles until reset; it is unsafe for long-running production services and mainly for experiments or microbenchmarks.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, …

  • Red Hat Certified System Administrator in Red Hat OpenStack
  • Certified Kubernetes Application Developer (CKAD)
  • Red Hat Certified Specialist in Ansible Automation
  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security