GOMAXPROCS, Ps, and What schedtrace Shows
GOMAXPROCS, Ps, and What schedtrace Shows
Thesis
GOMAXPROCS is the number of Ps — logical processors that can run Go code at once — not a hard cap on OS threads. When people set it “to the number of CPUs” they are usually right on bare metal and wrong in a CPU-limited container. This chapter prints the default, changes it in-process, and reads a schedtrace line so the idle/running P counts become visible.
Mental model
GOMAXPROCS = N → N Ps that execute goroutines
Ms (OS threads) ≥ N when work + syscalls + cgo demand it
schedtrace: idleprocs + running Ps ≈ gomaxprocs
Worked examples
Verified with Go 1.27.2.
Case 1: read and temporarily lower GOMAXPROCS
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
fmt.Println("NumCPU", runtime.NumCPU())
fmt.Println("GOMAXPROCS now", runtime.GOMAXPROCS(0))
old := runtime.GOMAXPROCS(2)
fmt.Println("set to 2; was", old)
done := make(chan struct{})
for i := 0; i < 8; i++ {
go func() {
for {
select {
case <-done:
return
default:
}
}
}()
}
time.Sleep(100 * time.Millisecond)
fmt.Println("NumGoroutine", runtime.NumGoroutine())
close(done)
runtime.GOMAXPROCS(old)
}GODEBUG=schedtrace=50 go run .On this 8-CPU guest the default GOMAXPROCS is 8. After setting 2, busy-spin goroutines still exist, but only two Ps run Go code at a time — schedtrace shows gomaxprocs=2 with fewer idleprocs under load.
Case 2: containers
Inside cgroups v2, Go 1.25+ already adjusts default GOMAXPROCS from the CPU quota when appropriate. Prefer fixing the quota over hard-coding GOMAXPROCS=1 in process env unless you are isolating a latency-critical sidecar.
Trap
GOMAXPROCS=1 as a “debugging” habit left in production. It serializes all Go code and hides races while destroying throughput. Use the race detector and schedtrace instead.
Boring rule
Leave the default alone unless a profile or a CPU quota says otherwise. When you must set it, set it from the visible CPU limit, not from NumCPU() of the host.
Try this
- Compare
runtime.GOMAXPROCS(0)on the host vs in a--cpus=1.5container. - Pair with the epoll/netpoller chapter under load.
- Read
runtime.GOMAXPROCSdocs for the latest cgroup behaviour.
Sources
runtime.GOMAXPROCS- Go release notes on cgroup-aware
GOMAXPROCS(1.25+)