Can a busy loop starve other goroutines?

Read the explanation
What if

One P, and main spins in a loop with no function calls. The runtime still interrupts it, so the ticker gets its turns.

Predict first

One P. main spins in a loop that makes no function calls. Does the ticker get a turn before the loop ends?

012345678910main: go ticker()gomain: time.Sleep(10ms)sleeptickbusy loopticker: time.Sleep(1ms): due, not firedsleeptickbusy loopticker: time.Sleep(1ms): due, not firedsleeptickbusy looptime.Sleep(1ms)print true

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
Step 1/5

Lanes are goroutines. With one P, only one of them runs Go code at a time.

1var ticks atomic.Int642 3func ticker() {4    for {5        ticks.Add(1)6        time.Sleep(time.Millisecond)7    }8}9 10func busy() (sum int) {11    for i := range 2_000_000_000 { // no function calls: no point where it stops on its own12        sum += i13    }14    return sum15}16 17func main() {18    runtime.GOMAXPROCS(1) // one P: one goroutine runs Go code at a time19    go ticker()20    time.Sleep(10 * time.Millisecond)21    before := ticks.Load()22    busy()23    fmt.Println("ticker ran during the loop:", ticks.Load() > before)24}

Explanation

What just happened

With GOMAXPROCS(1) there is one P, so only one goroutine runs Go code at a time. main took it for a loop that never calls a function, so the loop has no point where it stops on its own.

The runtime interrupted it anyway: async preemption. Each time, the P got to the scheduler, the ticker’s timer fired, and the ticker took a turn. Then main carried on where it left off.

Try the what-if switch

Why it works this way

Without preemption, a goroutine stops only where it checks: a function call with a stack check, or blocking. A tight loop has neither. Async preemption lets the runtime stop it anywhere, so one goroutine can’t freeze the rest.

Watch out

Turning preemption off brings the old risk back, and a call in the loop may not save you: the compiler leaves the check out of tiny leaf functions.

Common questions

Can an infinite loop in one goroutine block the others?

Not on its own. The runtime interrupts a running goroutine mid-loop (async preemption), even when the loop makes no function calls. With GOMAXPROCS(1), the ticker here still got its turns while main spun.

What does GODEBUG=asyncpreemptoff=1 do?

It stops the runtime from interrupting a running goroutine. A loop with no function calls then holds its P until it ends: here the ticker got no turn during the loop.

Why did my time.Sleep(time.Millisecond) take longer than 1 ms?

A sleeping goroutine's timer fires only when its P gets to the scheduler. While main held the only P, the ticker woke only at main's preemptions, not after 1 ms.

Verified: this timeline matches a real program, checked in CI on Go 1.26 and 1.27. How we verify. Unclear or wrong? Tell us