What is the runnext slot in Go's per-P scheduler, and what goes into it?
answer
- one slot in front of the queue
- LIFO at the front, FIFO behind
- filled by whoever did the waking
- cuts hand-off latency for chatty pairs
- it borrows the current time slice
basics
~20 srunnext is a single-goroutine fast slot on each P, holding the goroutine that P most recently made runnable. The P runs it ahead of its entire local run queue, and it inherits the remaining time slice rather than getting a fresh one.
solid answer
~50 sEach P has a one-entry `runnext` slot in front of its FIFO local run queue. Whenever the goroutine running on that P makes another goroutine runnable — a `go` statement, a channel send that wakes a blocked receiver, unlocking a `sync.Mutex` someone is waiting on — the readied goroutine goes into `runnext` and the previous occupant is bumped to the queue tail. The scheduler then runs `runnext` before anything in the local queue, so a wake-and-wait pair is scheduled as a unit instead of the woken goroutine queueing behind everything else; it is also still warm in that core's cache. The slot is bounded in two ways: the goroutine inherits the remaining time slice of the goroutine that readied it, so a hand-off chain cannot hold a P indefinitely, and another P will steal it only as a last resort on its final stealing pass.
code
go · 8 linesch := make(chan int)
// the new goroutine lands in this P's runnext slot
go func() { ch <- 42 }()
// this receive parks; the send readies it again, into the
// runnext slot of whichever P is running the sender
fmt.Println(<-ch) // prints: 42go deeper
Know that the scheduler keeps one just-readied goroutine in front of the queue, so a goroutine you have woken does not wait behind everything else on that processor.
Be able to say what fills the slot — any wake performed by the running goroutine, not just go statements — and what happens to the goroutine already sitting there.
Show that the fast slot is bounded: it borrows the current time slice and is stolen last, so it improves hand-off latency without letting a chatty pair monopolise a P.
The angle to own is that this latency optimisation belongs to the runtime; application tricks meant to 'help the scheduler' usually fight it, and the knowledge is diagnostic rather than actionable.
## The slot in front of the queue Every P — the scheduling context an OS thread must hold to run Go code — owns a FIFO local run queue of 256 entries, and in front of that queue sits a single-goroutine slot called `runnext`. It holds at most one goroutine, and the scheduler consults it before it looks at the queue. That makes the scheduler's short-term behaviour LIFO at the front and FIFO behind it. ## What fills it `runnext` is filled whenever the goroutine currently running on that P makes another goroutine runnable. That covers more cases than people expect: - a `go` statement — the new goroutine goes straight into `runnext`; - a send on a channel that a goroutine is blocked receiving from (and the mirror case: a receive that unblocks a blocked sender); - unlocking a `sync.Mutex` that another goroutine is waiting on; - a `sync.WaitGroup` counter reaching zero and releasing waiters; - a timer firing on that P and readying the goroutine waiting for it. In every case the runtime's ready path puts the newly runnable goroutine in the current P's `runnext` and pushes the previous occupant onto the tail of that P's local run queue. Nothing is lost; the displaced goroutine simply goes back to queueing normally. ## Why the runtime bothers The pattern `runnext` exists for is *communicate and wait*: goroutine A hands something to goroutine B and then blocks, expecting B to hand something back. Without a fast slot, B would be appended to the tail of a queue that might already hold dozens of goroutines, and the round trip would pay a full queue traversal in each direction. That latency is invisible when goroutines do milliseconds of work each and dominant when they do microseconds. Running B immediately also has a cache argument behind it: B is about to touch the data A just wrote, and it is warm in that core's caches right now. Scheduling the pair as a unit keeps it warm. The runtime's own comment on the field states the intent plainly: a set of goroutines locked in a communicate-and-wait pattern is scheduled as a unit, which removes the potentially large scheduling latency of appending each newly ready goroutine to the end of the run queue. ## What stops it becoming a monopoly Two bounds, and knowing them is what separates a middle answer from a senior one. **It borrows the time slice, it does not renew it.** When the scheduler runs the goroutine from `runnext`, it treats that as a continuation of the current time slice and does not advance the P's schedule tick. A chain of goroutines handing off to each other through `runnext` therefore counts as a single slice, and the runtime's monitor thread preempts the P once that slice has run for roughly ten milliseconds. Without this, two chatty goroutines could refresh their slice on every hand-off and hold a P forever. **It is stolen last, not first.** A P with nothing to do makes several randomised passes over the other Ps, taking about half of a victim's local run queue on each attempt. Only on its final pass will it consider taking the victim's `runnext`, and it pauses briefly before doing so to give the owner a chance to run it. That ordering preserves the fast slot's purpose — the hand-off it was created for — while still letting a genuinely idle P get the work eventually. ## Is the unfairness a problem? Over a few microseconds, yes, deliberately: the newest ready goroutine jumps ahead of older ones on that P. Over any longer horizon it evens out, because the time-slice inheritance caps a hand-off chain, other Ps steal from the *oldest* end of the local queue, and the scheduler periodically forces a look at the global run queue. ## What to do with this knowledge Almost nothing, in application code — and that is the honest answer to give. There is no API to influence `runnext`, `runtime.Gosched` only yields the current goroutine to the queue, and structuring code to "help the scheduler" usually fights it. The value of understanding the slot is diagnostic: it explains why a producer's own P tends to run the workers it wakes, why very short tasks concentrate rather than spread, and why an idle CPU can coexist with a backlog for a short window.
- Can a goroutine sitting in one P's runnext slot be taken by another P?Yes, but only as a last resort. A thief makes several randomised passes over the other Ps taking about half of a victim's local run queue; only on its final pass does it consider the victim's `runnext`, and it pauses briefly first to give the owner a chance to run it. That ordering keeps the fast slot doing its job while still letting an otherwise idle P get the work.
- Why does a goroutine run from runnext inherit the previous goroutine's time slice?Otherwise two goroutines handing off to each other would refresh the slice on every hop and could hold a P indefinitely. The runtime does not advance the P's schedule tick when it runs the `runnext` goroutine, so the whole chain counts as one slice and the runtime's monitor thread preempts it after roughly ten milliseconds.
- Doesn't jumping the queue break fairness for goroutines already waiting on that P?Over microseconds, yes, deliberately: the newest ready goroutine runs before older ones on that P. The bet is that it is the continuation of the work in flight and its data is cache-warm. Fairness returns through the time-slice inheritance, the scheduler's periodic forced look at the global run queue, and thieves taking from the oldest end of the local queue first.
saying these in an interview costs you the question
- Calls runnext just another entry in the local queue
- Says runnext can hold several goroutines
- Claims a runnext goroutine gets a fresh full time slice
- Thinks the local run queue is strictly FIFO end to end
- Says only the go statement can fill runnext