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

Related Discussions

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