Golang Garbage Collection Explained: GC, Tuning, Pause Time & Examples

Golang Garbage Collection Explained: GC, Tuning, Pause Time & Examples

Golang uses an efficient and concurrent garbage collector to automatically manage memory and reduce manual overhead. Understanding how Go’s GC works, how to tune it, and how to monitor performance is essential for building high-performance applications. This guide covers GC internals, tuning techniques, pause time behavior, and real-world optimization examples.


Golang Garbage Collection Quick Cheat Sheet

Command / ConceptDescription
runtime.GC()Manually trigger garbage collection
runtime.ReadMemStats(&m)Get memory and GC statistics
m.HeapAllocCurrently allocated heap memory
m.NumGCNumber of GC cycles executed
GOGC=100Default GC target percentage
GOGC=50Run GC more frequently (lower memory usage)
GOGC=200Run GC less frequently (better performance)
debug.SetGCPercent(n)Change GC percentage at runtime
GODEBUG=gctrace=1Enable detailed GC logs
GODEBUG=gctrace=1 go run main.goRun program with GC tracing
pprofAnalyze memory and GC behavior
HeapIdleUnused heap memory
HeapReleasedMemory returned to OS
Stop-the-world (STW)Brief pause during GC phases
Concurrent GCGC runs alongside application
Tri-color markingAlgorithm used by Go GC

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
}

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")
}

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)
}

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:

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("-----------")
}

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()
}

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:

go
# GODEBUG=gctrace=1 go run main.go a

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.

FieldMeaning
gc 1GC cycle number
@0.009sTime since program start when GC occurred
2%Percentage of CPU time spent in GC
0.022+0.49+0.17 msGC phases: stop-the-world + mark + sweep
3->4->0 MBHeap size (before → after → live memory)
4 MB goalTarget heap size for next GC
4 PNumber 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
}

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 (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.


Frequently Asked Questions

1. What is garbage collection in Golang?

Golang uses an automatic garbage collector to manage memory by reclaiming unused objects and reducing manual memory handling.

2. How to force garbage collection in Go?

You can manually trigger garbage collection using the runtime.GC() function in Go.

3. What is GOGC in Golang?

GOGC is an environment variable that controls how frequently garbage collection runs based on heap growth percentage.

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

Yes, Golang uses a concurrent GC with very short stop-the-world pauses during specific phases like root scanning.

5. Can we disable garbage collection in Go?

Yes, garbage collection can be disabled temporarily using debug.SetGCPercent(-1), but it is not recommended for production use.

Summary

Golang’s garbage collector is a powerful, concurrent memory management system that simplifies development while maintaining high performance. By understanding how GC works, monitoring memory usage, and tuning parameters like GOGC, developers can significantly improve application efficiency and reduce latency.

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 valuable insights for optimizing memory usage.


Official Documentation

Deepak Prasad

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, networking, and security, delivering robust and efficient solutions for diverse projects.