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 / 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 |
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.
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.
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:
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:
# go run main.go
Initial HeapAlloc: 1654512
After GC HeapAlloc: 37872
After release HeapAlloc: 37872GC 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.
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.
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:
GC pause duration: 154.197µsThis 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:
GODEBUG=gctrace=1 go run main.goExample:
GOGC=50 go run main.goLower 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.
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 useHeapIdle→ unused heap memoryHeapReleased→ memory returned to OSNumGC→ number of GC cycles
👉 Use this to detect memory leaks and GC inefficiencies.
# 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.
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:
# 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: interruptSo, 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:
# GODEBUG=gctrace=1 go run main.go aOutput:
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 PThis 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.
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/pprofexposes 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.
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 growthGOMEMLIMIT
Triggers GC when memory usage approaches a defined limit
GOGC=100 GOMEMLIMIT=512MiB go run main.goThis 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:
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:
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.
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):
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 = 0In this example, memory keeps increasing and NumGC remains 0, which confirms that garbage collection is disabled.
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.


