❝ Fast, Good, Cheap - pick any two ❞

Follow me on GitHub

1. Introduction

  • It sets how many OS threads (CPU cores) Go can run goroutines on at the same time.
  • Default = number of CPU cores on your machine.

Without setting GOMAXPROCS

If you have 8 CPU cores, Go will schedule goroutines across all 8 by default.

Otherwise, the Go runtime selects an appropriate default value from a combination of

  • the number of logical CPUs on the machine,
  • the process’s CPU affinity mask,
  • and, on Linux, the process’s average CPU throughput limit based on cgroup CPU quota, if any. ***

2. Usage Examples

Example

runtime.GOMAXPROCS(1) // use only 1 core

👉 Even if you have 8 cores, all goroutines will share just 1 core, running concurrently but not in true parallel.

runtime.GOMAXPROCS(4) // use 4 cores

👉 Go can schedule goroutines on 4 different cores simultaneously → parallelism.


Key idea

  • Concurrency = many tasks in progress (goroutines).
  • Parallelism = many tasks running at the same time (needs multiple CPU cores).
  • GOMAXPROCS controls how much parallelism you allow.

✅ You usually don’t need to change it. Only tweak when:

  • Benchmarking parallel vs single-core performance.
  • Running in environments with CPU limits (like Docker cgroups).

3. Comparation

CPU-Bound

Firstly I define a function as CPU-bound task

func runCPUBoundTask() {
	const iter = 1_000_000
	start := time.Now() // start timer
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go cpuWork(iter, &wg)
	}

	wg.Wait()
	fmt.Println("runCPUBoundTask took:", time.Since(start))
}

// cpuWork runs heavy floating-point work
func cpuWork(iter int, wg *sync.WaitGroup) {
	defer wg.Done()

	sum := 0.0
	for i := 0; i < iter; i++ {
		sum += float64(i) * 0.000001
		if sum > 1e9 {
			sum = 0
		}
	}
	fmt.Println("Result:", sum) // prevent compiler optimization
}

then in main.go I set GOMAXPROCS to 1 first experiment

func main() {
	runtime.GOMAXPROCS(1)
	runCPUBoundTask()
}

Then I update GOMAXPROCS to 4 & 8. Each time change GOMAXPROCS I run experiment 3 times to get execution times. Then we have a comparasion table as below

GOMAXPROCS First Time Second Times Third Times
1 24.215125 ms 24.308458 ms 23.794709 ms
4 8.678666 ms 8.489667 ms 6.727209 ms
8 5.086542 ms 5.024125 ms 5.08575 ms

I/O-Bound

Firstly I define a function as I/O-bound task

func runIOBloundTask() {
	start := time.Now() // start timer
	var wg sync.WaitGroup

	for i := 1; i <= 10; i++ {
		wg.Add(1)
		go callAPI(fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", i), &wg)
	}

	wg.Wait()
	fmt.Println("runIOBoundTask took:", time.Since(start))
}

func callAPI(url string, wg *sync.WaitGroup) {
	defer wg.Done()

	resp, err := http.Get(url)
	if err != nil {
		fmt.Printf("Request to %s failed: %v\n", url, err)
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Printf("Response from %s:\n%s\n\n", url, string(body)[:100]) // Print first 100 chars
}

then in main.go I set GOMAXPROCS to 1 first experiment

func main() {
	runtime.GOMAXPROCS(1)
	runIOBloundTask()
}

Then I update GOMAXPROCS to 4 & 8. Each time change GOMAXPROCS I run experiment 3 times to get execution times. Then we have a comparasion table as below

GOMAXPROCS First Time Second Times Third Times
1 122.841417 ms 123.66525 ms 123.409875 ms
4 128.13425 ms 118.5735 ms 130.491875 ms
8 137.731375 ms 126.189 ms 130.6665 ms

4. Conclusion

I/O-bound Example (API calls)

Here’s 3 goroutines (G1, G2, G3) making HTTP requests when GOMAXPROCS=1

Time → ─────────────────────────────────────────────────────────>

G1: |Send Req|====WAIT====|Process Resp|
G2:           |Send Req|====WAIT====|Process Resp|
G3:                     |Send Req|====WAIT====|Process Resp|
  • |Send Req| and |Process Resp| → short CPU bursts
  • ====WAIT==== → blocked on network (no CPU needed)
  • 👉 Even with 1 CPU, while G1 is waiting, CPU is free for G2/G3 to run. So they overlap nicely.
  • ⏱️ Total time ≈ time of the slowest request.

CPU-bound Example (heavy computation)

Now, if each goroutine runs a big computation loop (no I/O):

With GOMAXPROCS=1

Time → ─────────────────────────────────────────────────────────>

G1: [Compute...............]
G2:                        [Compute...............]
G3:                                             [Compute...............]
  • Each goroutine hogs the only CPU, so they run one after the other.
  • ⏱️ Total time ≈ sum of all workloads.

With GOMAXPROCS=3 (3 CPUs available)

Time → ─────────────────────────────────────────────────────────>

CPU1: [Compute...............] (G1)
CPU2: [Compute...............] (G2)
CPU3: [Compute...............] (G3)
  • All goroutines run at the same time on different CPUs.
  • ⏱️ Total time ≈ one goroutine’s workload.
  • Huge speedup.

✅ So the big insight is:

  • I/O-bound tasks overlap well even on 1 CPU, because goroutines are paused while waiting.
  • CPU-bound tasks need multiple CPUs to run in true parallel, otherwise they serialize.