After reading arguments against setting CPU limits, I stopped configuring them and now use CPU requests plus memory requests and limits. So far this has worked well: workloads that used to be throttled run more smoothly without appearing to consume excessive CPU.
My concern is how this affects Go services. Without a CPU limit, a Go process may see all the cores on its node and choose a GOMAXPROCS value based on that number. For example, a small service using roughly 512 millicores could run on a 16-core node and appear to have 16 CPUs available. Could that result in unnecessary OS threads, excessive context switching, or added latency?
Is this a measurable production concern? If it is, what is the best way to manage it across many deployments, especially for third-party open-source services where I cannot change the code? Manually setting GOMAXPROCS everywhere seems difficult, so I am also considering an admission policy that injects it automatically.
4 Answers
Go's behavior also depends on the Go version. Recent Go releases have container-aware GOMAXPROCS behavior, but the default is tied to the CPU limit when one is available. If there is no CPU limit, it can fall back to the host's CPU count, so removing limits does not automatically make the runtime follow the CPU request.
For older versions or software that needs predictable parallelism, explicitly setting GOMAXPROCS or injecting a suitable runtime configuration can be reasonable. Test it against the application's latency and throughput rather than assuming that fewer parallel workers is always better.
CPU limits are mainly a scheduling and isolation choice, not a way to pin a process to a number of cores. Removing them can allow short bursts to use idle capacity instead of being throttled, but requests still need to be realistic. If a pod regularly uses far more CPU than its request, many replicas can be packed onto a node and the resulting contention will hurt everyone.
The right answer depends on workload class. Latency-sensitive or highly CPU-intensive services may need explicit limits, tuned GOMAXPROCS values, or dedicated nodes. For ordinary controllers and services, accurate requests, autoscaling, and monitoring are often sufficient.
If you do need GOMAXPROCS to follow the container's intended CPU allocation, there are fleet-wide options. You can use a container-aware helper such as automaxprocs, or use an admission policy like Kyverno to inject the setting into deployments. That is much less tedious than editing every manifest by hand.
Just be careful about what value you choose. A hardcoded value such as 4 may prevent vertical scaling from being used effectively. CPU requests should reflect realistic usage, and autoscaling can handle workloads that need more capacity.
Usually this is not something to worry about by itself. GOMAXPROCS controls how many goroutines can execute Go code in parallel; it does not simply create one permanently busy OS thread for every visible CPU. The runtime grows OS threads when goroutines block or become unschedulable, and its M:N scheduler generally keeps context switching relatively efficient.
A service that is mostly idle and uses about 512 millicores should not suddenly thrash just because the node has 16 cores. Monitor CPU usage, throttling, latency, runnable goroutines, and context-switch metrics rather than assuming a problem from the GOMAXPROCS value alone. For most workloads, accurate CPU requests and avoiding unnecessary throttling are more important.
That addresses my main concern. I have added monitoring for context switching and latency, so I can verify whether any workload actually shows a problem instead of changing everything preemptively.

An admission policy was my preferred approach too. The nodes are not resource constrained; I was mainly trying to avoid introducing latency from a mismatch between a small workload and a large node.