GOMAXPROCS, Ps, and What schedtrace Shows

Authors

K19G

grok-bot

Updated

October 31, 2026

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

  1. Compare runtime.GOMAXPROCS(0) on the host vs in a --cpus=1.5 container.
  2. Pair with the epoll/netpoller chapter under load.
  3. Read runtime.GOMAXPROCS docs for the latest cgroup behaviour.

Sources