Go runs many goroutines across operating-system threads, but a thread needs a runtime resource called a P before it can execute Go code. The number of Ps equals the current GOMAXPROCS value, so GOMAXPROCS limits how many threads can execute Go code simultaneously—not how many goroutines or OS threads a program can have. Since Go 1.25, the default can also account for container CPU limits, rather than simply reflecting the machine’s logical CPU count.
How does the Go scheduler work?
The runtime’s job is to distribute ready-to-run goroutines across worker threads. The common G-M-P model is a useful way to understand how it does that:
As an Amazon Associate I earn from qualifying purchases.
| Letter | Resource | What it represents | What to remember |
|---|---|---|---|
| G | Goroutine | A unit of Go work that can be runnable, running, waiting, or otherwise not ready to run. | Programs can have many more goroutines than can run at once. |
| M | Operating-system thread | A thread on which the runtime executes work. | An M needs a P to execute Go code, but it can be blocked in a system call without holding a P. |
| P | Runtime processor resource | The runtime resources needed to execute Go code, including scheduler and memory allocator state. | The runtime has as many Ps as the current GOMAXPROCS value. A P is not a physical CPU core. |
“M:N” is shorthand for multiplexing many goroutines over a set of OS threads. It is incomplete on its own: the P explains what limits simultaneous Go execution and why goroutine count, thread count, and execution capacity are different quantities.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →What happens when work blocks?
A goroutine that waits—for example, for a channel operation or other event—cannot use an execution slot while it is waiting. The runtime can schedule other runnable work instead. An M blocked in a system call can exist without a P, allowing the P to be used for other Go work. This is why neither the number of goroutines nor the number of OS threads directly tells you how many goroutines are executing Go code at a given moment.
How does work stealing work in Go?
The runtime maintains distributed scheduler state, including work queues associated with Ps. If a worker has no local work, the scheduler can look for runnable goroutines or timer work associated with another P. In the current runtime source, stealWork attempts to steal runnable goroutine or timer work from any P.
Work stealing helps the runtime find work when runnable tasks are unevenly distributed. It does not guarantee a particular goroutine order, a fixed fairness bound, or queues of equal length. Those are not promises developers should build application behavior around.
Why does the runtime park and wake workers?
Keeping every worker active can waste CPU when there is little useful work; keeping too many workers parked can delay work that becomes runnable. The runtime balances worker parking and waking against those competing costs. Its scheduler state is distributed, and future work cannot be known perfectly, so the exact worker behavior is an implementation detail rather than a stable scheduling contract.
What does GOMAXPROCS actually control?
The Go runtime documentation defines GOMAXPROCS as the maximum number of CPUs that can be executing simultaneously. In the G-M-P model, that capacity corresponds to the number of Ps: an M needs a P to run Go code, and the runtime has as many Ps as the current GOMAXPROCS setting.
For example, the Go blog uses GOMAXPROCS=8 and 1,000 runnable goroutines to illustrate that Go can use eight threads to run eight goroutines at a time. Those numbers are an explanatory example, not a benchmark. The important distinction is that runnable goroutines can outnumber simultaneous Go execution slots.
Does GOMAXPROCS limit goroutines or OS threads?
No. It sets Go execution parallelism, not a goroutine quota or a cap on all OS threads. A program may have many goroutines waiting or ready to run, and the runtime may have OS threads that are idle or blocked in system calls. Only an M with a P can execute Go code.
Nor does a P represent a reserved physical core. GOMAXPROCS describes the runtime’s available parallelism; it does not reserve CPU time or guarantee a share of the host processor. Container CPU quotas and operating-system scheduling still affect how much CPU time a process actually receives and the latency it experiences.
Free tools Windows power users keep installed
One-click scans. No signup required.
How did Go’s default GOMAXPROCS change in Go 1.25?
From Go 1.5 through Go 1.24, the default was based on the machine’s logical CPU count. Go 1.25 introduced a container-aware default intended to account better for CPU limits. Current runtime documentation describes the default as potentially considering logical CPU count, process CPU affinity, and—on Linux—the process’s average CPU throughput limit based on its cgroup quota. The runtime can periodically update its default when relevant conditions change.
| Go version context | Default behavior | Practical implication |
|---|---|---|
| Go 1.5–1.24 | Based on the machine’s logical CPU count. | A container could see a value based on host CPU availability rather than its CPU quota. |
| Go 1.25 and later, according to current runtime documentation | Can consider logical CPUs, process affinity, and Linux cgroup CPU throughput limits; the default can be updated periodically as relevant conditions change. | The default may better reflect the process’s effective CPU environment, but it does not reserve that CPU capacity. |
The Go blog describes the intended meaning this way: “Semantically, GOMAXPROCS tells the Go runtime the ‘available parallelism’ that Go should use.” For a containerized application, the default therefore depends on more than the host’s advertised CPU count.
Rank #4
What if GOMAXPROCS is set explicitly?
Setting GOMAXPROCS through the environment or calling runtime.GOMAXPROCS explicitly disables automatic updates to the default, according to the runtime package documentation. The package also documents runtime.SetDefaultGOMAXPROCS as a way to restore default behavior.
Compatibility settings matter as well. The current documentation describes GODEBUG=containermaxprocs=0 and GODEBUG=updatemaxprocs=0; their defaults are tied to language version 1.24 and earlier. Check the runtime documentation for the Go version and language version your program uses before relying on their behavior.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesHow should you choose or check GOMAXPROCS?
Do not hard-code a value solely because it matches the host’s logical CPU count, or assume every container needs a manual override. First identify the Go version, any explicit configuration, and the process’s CPU environment:
Best Value
- Check the Go version. Determine whether the program uses Go 1.25 or later, when container-aware defaults were introduced.
- Check for an override. Look for the
GOMAXPROCSenvironment variable, calls toruntime.GOMAXPROCS, and relevantGODEBUGsettings. An explicit setting disables automatic default updates. - Check the process’s CPU constraints. Establish whether CPU affinity or, on Linux, a cgroup quota affects the process. These can matter to the default.
- Measure the application under its real workload. Compare performance and latency in the environment where the program runs before changing parallelism. A setting that suits one workload or deployment may not suit another.
How can you inspect scheduler activity?
The Go performance wiki documents two scheduler trace settings:
GODEBUG=schedtrace=1000prints a scheduler trace every 1,000 milliseconds.GODEBUG=schedtrace=1000,scheddetail=1requests detailed scheduler output at the same interval.
Trace output can expose values such as GOMAXPROCS, idle processors, worker threads, global run-queue length, and local per-P queues. Use it as evidence to investigate a workload, not as a diagnosis by itself: queue and worker counts need to be interpreted alongside application measurements and other profiling information.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →




