Go version
go version go1.24.2 windows/amd64
Output of go env
in your module/workspace:
set AR=ar
set CC=gcc
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_ENABLED=0
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set CXX=g++
set GCCGO=gccgo
set GO111MODULE=
set GOAMD64=v1
set GOARCH=amd64
set GOAUTH=netrc
set GOBIN=
set GOCACHE=C:\Users\maste\AppData\Local\go-build
set GOCACHEPROG=
set GODEBUG=
set GOENV=C:\Users\maste\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFIPS140=off
set GOFLAGS=
set GOGCCFLAGS=-m64 -fno-caret-diagnostics -Qunused-arguments -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\maste\AppData\Local\Temp\go-build2732114703=/tmp/go-build -gno-record-gcc-switches
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMOD=NUL
set GOMODCACHE=D:\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=D:\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=D:\Program Files\Go
set GOSUMDB=sum.golang.org
set GOTELEMETRY=local
set GOTELEMETRYDIR=C:\Users\maste\AppData\Roaming\go\telemetry
set GOTMPDIR=
set GOTOOLCHAIN=auto
set GOTOOLDIR=D:\Program Files\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.24.2
set GOWORK=
set PKG_CONFIG=pkg-config
What did you do?
package main
import (
"log"
"runtime"
"sync"
"time"
)
func cb(in *sync.WaitGroup) {
in.Wait()
}
func printStats(msg string) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("%s: Goroutines=%d, Alloc=%d MB, Sys=%d MB, HeapAlloc=%d MB, HeapSys=%d MB",
msg, runtime.NumGoroutine(), m.Alloc/1024/1024, m.Sys/1024/1024, m.HeapAlloc/1024/1024, m.HeapSys/1024/1024)
}
func main() {
tasks := 2_000_000
first := sync.WaitGroup{}
first.Add(1)
for i := 0; i < tasks; i++ {
go cb(&first)
if i%100_000 == 0 {
log.Println("Created goroutines:", i)
}
}
printStats("Before first.Done()")
first.Done()
time.Sleep(1 * time.Second)
printStats("After first.Done(), before GC")
runtime.GC()
time.Sleep(1 * time.Second)
printStats("After GC")
// time.Sleep(1 * time.Minute) // Memory is not released over time
// debug.FreeOSMemory() // Can release leaked memory, but should not be used in production
}
What did you see happen?
When creating a very large number of goroutines (e.g., millions) that all block on a single sync.WaitGroup
or similar synchronization primitive, the goroutines never seem to release memory even after all have returned. The goroutines appear to be pooled internally by the runtime, and memory usage stays high until a manual call to debug.FreeOSMemory()
.
This behavior can be observed even if all goroutines have finished executing and runtime.GC()
has been called.
- Goroutine count drops but memory remains high.
- Even after
runtime.GC()
, a large amount of memory is retained. - Only
debug.FreeOSMemory()
can reduce memory. - Goroutine creation speed is extremely slow, approximately 400,000 goroutines per second, significantly lower than expected.
$ time go run test.go
2025/09/13 21:38:40 Created goroutines: 0
2025/09/13 21:38:40 Created goroutines: 100000
2025/09/13 21:38:41 Created goroutines: 200000
2025/09/13 21:38:41 Created goroutines: 300000
2025/09/13 21:38:41 Created goroutines: 400000
2025/09/13 21:38:41 Created goroutines: 500000
2025/09/13 21:38:42 Created goroutines: 600000
2025/09/13 21:38:42 Created goroutines: 700000
2025/09/13 21:38:42 Created goroutines: 800000
2025/09/13 21:38:43 Created goroutines: 900000
2025/09/13 21:38:43 Created goroutines: 1000000
2025/09/13 21:38:43 Created goroutines: 1100000
2025/09/13 21:38:43 Created goroutines: 1200000
2025/09/13 21:38:44 Created goroutines: 1300000
2025/09/13 21:38:44 Created goroutines: 1400000
2025/09/13 21:38:44 Created goroutines: 1500000
2025/09/13 21:38:44 Created goroutines: 1600000
2025/09/13 21:38:45 Created goroutines: 1700000
2025/09/13 21:38:45 Created goroutines: 1800000
2025/09/13 21:38:45 Created goroutines: 1900000
2025/09/13 21:38:45 Before first.Done(): Goroutines=2000001, Alloc=1112 MB, Sys=16893 MB, HeapAlloc=1112 MB, HeapSys=1134 MB
2025/09/13 21:38:47 After first.Done(), before GC: Goroutines=1, Alloc=1112 MB, Sys=16893 MB, HeapAlloc=1112 MB, HeapSys=1134 MB
2025/09/13 21:38:48 After GC: Goroutines=1, Alloc=871 MB, Sys=16898 MB, HeapAlloc=871 MB, HeapSys=16753 MB
real 0m10.121s
user 0m0.091s
sys 0m0.061s
What did you expect to see?
- All goroutines should exit and their memory should be reclaimed by GC.
- Memory usage should drop close to the baseline after all goroutines return.
- Goroutine creation should be extremely fast.
Comment From: seankhliao
As the documentation for debug.FreeOSMemory
notes, it will gradually do this over time.
Immediately releasing to the OS and then re-acquiring later is much more expensive.
Unlike many projects, the Go project does not use GitHub Issues for general discussion or asking questions. GitHub Issues are used for tracking bugs and proposals only.
For questions please refer to https://github.com/golang/go/wiki/Questions
Comment From: gabyhelp
Related Issues
- runtime.malg: memory leak #66564 (closed)
- go http concurrency and Goroutines do not release memory!!! #15590 (closed)
- runtime: memory of finished goroutines not freed #65864 (closed)
- runtime: 1.3 garbage collector not releasing server memory back to system from finished goroutines #8287 (closed)
- runtime: potential gc deadlock #68373 (closed)
- runtime: SetFinalizer causes undesirable memory overhead #45581 (closed)
- runtime: potential memory leak in Go1.11.2 #29185 (closed)
Related Discussions
- Strange blocking in allocations during GC
- Memory leak when calling highly parallel goroutines
- Memory usage of goroutines
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: System233
As the documentation for
debug.FreeOSMemory
notes, it will gradually do this over time. Immediately releasing to the OS and then re-acquiring later is much more expensive.Unlike many projects, the Go project does not use GitHub Issues for general discussion or asking questions. GitHub Issues are used for tracking bugs and proposals only.
For questions please refer to https://github.com/golang/go/wiki/Questions
So after calling FreeOSMemory
, what accounts for the remaining \~1GB memory usage in the process? At this point, there are almost no variables in scope.
Also, there is a goroutine creation performance issue: in Node.js, starting 2 million blocking Promise tasks takes less than 2 second, whereas in Go it takes around 8 seconds.
Based on the above data: 2 million goroutines consuming 16 GB of memory, each goroutine uses up to 8 KB, far higher than the commonly cited 2 KB. It's time to update our understanding…