skip to content

Parking and Waking Goroutines

Every blocking operation ends in the same place: the goroutine is parked on a wait queue, its M goes looking for other work, and whoever unblocks it marks it runnable again.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

When a goroutine blocks on a channel receive, what happens to the OS thread it was running on?

level: juniorimportance: must knowfreq 62%

answer

  1. the thread does not wait with it
  2. state moves from running to waiting
  3. a record is left on the channel's queue
  4. only another goroutine can make it runnable
  5. cost is a small stack, not a thread

basics

~20 s

Nothing blocks at the operating-system level. Go's runtime parks the goroutine, marking it waiting and detaching it from the thread, then runs another runnable goroutine on that same thread. The thread keeps doing useful work.

solid answer

~40 s

The thread is not blocked at all. When the receive cannot proceed, the runtime **parks** the goroutine: its internal `gopark` routine moves that goroutine from *running* to *waiting*, records the wait reason (`chan receive`), leaves a small wait record on the channel's receive queue, and re-enters Go's goroutine scheduler on the same OS thread, which immediately picks another runnable goroutine to run. Nothing polls the parked goroutine; it becomes runnable again only when another goroutine sends on that channel (or closes it) and the runtime explicitly readies it. That is why blocking code is idiomatic in Go: a parked goroutine costs its stack — a couple of kilobytes to start — plus a small wait record, not an OS thread, so a server can hold tens of thousands of them.

go deeper

for a junior

Be ready to say that a goroutine blocking on a channel does not block an OS thread: the runtime sets it aside and runs something else on that thread. That single fact is what the question is testing.

for a middle

Explain the mechanics in the runtime's terms: the goroutine moves from running to waiting, leaves a wait record on the channel, and the thread re-enters the scheduler. Contrast parking with yielding, where the goroutine stays runnable.

for a senior

Show you know the operational consequences: parked goroutines cost stacks and keep their referenced data alive as GC roots, nothing times them out, and a climbing goroutine count usually means parks with no counterpart coming.

for a principal

Frame it as a design budget. Cheap parking is why blocking, goroutine-per-request code is the Go default rather than callbacks — but it makes "who eventually wakes this goroutine" an ownership question your APIs must answer, not an afterthought.

## What "parking" means Go multiplexes many goroutines onto a small number of OS threads. In the runtime's own vocabulary a **G** is a goroutine, an **M** is an OS thread, and a **P** is the scheduling context an M must hold to run Go code. A goroutine only ever runs while it sits on top of an M. When a goroutine executes `v := <-ch` and no value is available, the runtime does **not** leave the thread sitting in a kernel wait. It **parks** the goroutine. Concretely, the channel receive path calls the runtime's internal `gopark`, which: 1. flips the goroutine's state from *running* to *waiting*; 2. records a **wait reason** — a short string such as `chan receive`, `chan send`, `select` or `semacquire` — which is what a goroutine dump later prints next to that goroutine; 3. leaves a small wait record for the goroutine on the channel's receive queue, so the counterpart can find it; 4. unlinks the goroutine from the M and jumps back into the scheduler on that same thread. The scheduler then picks the next runnable goroutine and runs it on the thread that was just freed. From the operating system's point of view, nothing blocked: the thread never stopped executing. ## How it becomes runnable again Waking is always caused by another goroutine, never by the passage of time and never by polling. When some other goroutine sends on that channel, the send path finds the waiting record, completes the value transfer, and calls the runtime's `goready` on the parked goroutine, which flips it back from *waiting* to *runnable* and puts it on a run queue so a thread will pick it up shortly. Closing the channel does the same thing for every goroutine parked receiving on it. The important consequence: a parked goroutine is invisible to Go's goroutine scheduler. It is not retried, not timed out, and not checked periodically. If no counterpart ever arrives, it stays parked for the life of the process. ## Why this makes goroutines cheap Compare with a language that maps each concurrent task onto its own OS thread: | | blocked OS thread | parked goroutine | |---|---|---| | who is blocked | the kernel thread | nothing at OS level | | switch cost | a kernel context switch | user-space bookkeeping, in the hundreds of nanoseconds | | memory | a stack reservation typically measured in megabytes | a growable stack starting at a couple of kilobytes, plus one wait record | | how many | thousands is already painful | hundreds of thousands is routine | This is the whole reason "one goroutine per connection" or "one goroutine per request" is normal Go design, while "one thread per connection" is a known scaling problem elsewhere. Writing straight-line blocking code is idiomatic here precisely because the block is a user-space park, not a thread being taken out of service. ## What a parked goroutine still costs Parking is cheap, not free. A goroutine parked on a channel receive still holds: - **its stack**, which is also a GC root — everything its live frames reference stays reachable and cannot be collected; - **its wait record** on the channel's queue; - **its slot in the goroutine count**, which is why a goroutine total that only climbs is a classic symptom of goroutines parked on something that will never happen. So "goroutines are cheap" means *cheap while they eventually finish*. A goroutine parked forever is a permanent memory cost, not a free one. ## Parking versus other ways a goroutine stops Several things can interrupt a running goroutine, and only one of them is parking: - **Parking** (a channel operation that cannot proceed, a contended mutex, a `select` with nothing ready): the goroutine becomes *waiting* and needs another goroutine to ready it. - **`runtime.Gosched()`**: the goroutine yields the thread but stays **runnable** — it goes back on a run queue and will run again with no help from anyone. - **Preemption**: the runtime takes the thread away involuntarily; again the goroutine stays runnable. Mixing these up is the usual misconception. "The goroutine is blocked" in Go does not imply anything is stuck at the OS level, and it does not imply spinning either — a parked goroutine burns no CPU at all while it waits. ## What you can observe A goroutine dump shows the state in brackets on the goroutine's header line, for example `goroutine 42 [chan receive]:` followed by the stack. That bracketed text is the wait reason recorded at park time, so it tells you exactly which kind of park a goroutine is sitting in — and, taken across a whole dump, how many goroutines are parked on the same thing.

  • What actually makes that parked goroutine runnable again?
    Another goroutine doing the counterpart operation on the same channel. Its send (or a close) finds the wait record, transfers the value, and calls the runtime's `goready`, which moves the parked goroutine back to *runnable* and puts it on a run queue. There is no timer and no polling — without that counterpart, nothing wakes it.
  • How is parking different from calling runtime.Gosched?
    `runtime.Gosched()` yields the thread but leaves the goroutine **runnable**, so the scheduler will run it again on its own. Parking makes the goroutine **waiting**, which the scheduler ignores entirely until some other goroutine readies it. Yielding is a hint about fairness; parking is a claim that there is nothing to do yet.
  • What does a goroutine parked on a channel receive still consume?
    Its goroutine stack (which starts small but is a GC root, so everything its frames reference stays alive), one wait record on the channel's queue, and one entry in the process's goroutine count. It consumes no CPU and no OS thread while it waits.

A waiter does not stand at your table while the kitchen cooks. He notes your order on a ticket, serves other tables, and comes back only when the kitchen calls the ticket.

saying these in an interview costs you the question

  • Says the OS thread blocks alongside the goroutine
  • Thinks a blocked goroutine spins or busy-waits until data arrives
  • Claims Go's scheduler periodically retries blocked goroutines
  • Says goroutines are just OS threads with smaller stacks
  • Assumes a parked goroutine is garbage collected if nothing sends
open as a page

What does Go's runtime store on a channel so it knows which goroutine to wake?

level: middleimportance: should knowfreq 34%

basics

~20 s

Go's runtime queues a wait record, called a sudog, for each blocked goroutine: it names the goroutine and the address the value goes to. A counterpart operation pops the first record from that channel's FIFO queue and readies its goroutine.

open as a page

A multiplexer goroutine has been parked on a channel send for hours. What could ever wake it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Only another goroutine receiving from that channel, or closing it. A park carries no timeout and Go's scheduler never revisits waiting goroutines, so once the caller has gone, the send stays parked for the life of the process.

open as a page

When a sender finds a receiver already parked on a channel, where is the value copied?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

The sending goroutine copies the value straight into the parked receiver's destination variable, using the address in that receiver's wait record, then marks the receiver runnable. The channel's buffer is not used, even on a buffered channel.

open as a page