When a goroutine calls time.Sleep, what does the Go runtime block, and what wakes it?
answer
- count the threads, not the goroutines
- the deadline goes onto a heap
- one blocking OS wait covers every timer
- at least the duration, never exactly
basics
~20 sNothing at the OS level blocks. The Go runtime parks that goroutine and records its wake time in a runtime timer heap, freeing the thread for other goroutines; when the deadline passes the goroutine becomes runnable again.
solid answer
~50 s`time.Sleep` does not block a thread and does not create an OS timer. The runtime parks the calling goroutine, which means it is taken off the run queue and marked as waiting, and stores a timer holding its wake-up time in a min-heap owned by the scheduling context (a P) the goroutine was running on. The OS thread underneath is immediately free to run other goroutines. Go's goroutine scheduler checks that heap on its scheduling passes, and when a thread is about to go idle it blocks in the netpoller with a timeout set to the earliest pending deadline, so a single OS-level wait covers every pending timer. When the deadline arrives the runtime marks the goroutine runnable and it queues like any other work. That is why `Sleep` guarantees *at least* the requested duration and never exactly it, and why hundreds of thousands of sleeping goroutines are cheap.
code
go · 6 linesfor i := 0; i < 100_000; i++ {
go func() {
time.Sleep(time.Minute)
}()
}
// each sleeper costs a growable stack and one runtime timer entry, not a threadgo deeper
Be ready to say that a sleeping goroutine holds no OS thread and that the Go runtime, not the kernel, remembers the wake-up time. Knowing that Sleep means at least the duration is enough at this level.
Explain the mechanics end to end: the goroutine is parked, its wake time goes onto a runtime timer heap, the thread returns to the scheduler, and the runtime folds all pending deadlines into the timeout of one blocking wait.
Show you reason about wake-up latency, not just correctness. A timer only makes a goroutine runnable, so under load sleeps return late, and you should be able to say how you would measure that delay before blaming the timer.
Own the design consequence: anything built on sleeps and timers is a lower bound, never a schedule. Retry budgets, deadline arithmetic and SLOs must tolerate late wake-ups, and mass-expiring timers need jitter to avoid a synchronised herd.
## The question behind the question Engineers arriving from a thread-per-request language read `time.Sleep(30 * time.Second)` and wince: thirty seconds of a thread doing nothing. In Go that instinct is wrong, and understanding why is the entry point into how the runtime keeps time. ## What actually happens on a Sleep Go multiplexes many goroutines (Gs) onto a smaller number of OS threads (Ms), each of which needs a scheduling context (a P) to run Go code. `time.Sleep` calls into the runtime and does two things: 1. **It parks the calling goroutine.** The goroutine's state changes from running to waiting, and it is not on any run queue, so the scheduler will not pick it up. Its stack and its saved registers stay on the heap, waiting to be resumed. 2. **It records a timer.** The runtime creates a small timer object holding the wake-up instant (a monotonic-clock value, not a wall-clock time) and pushes it onto a min-heap of pending timers ordered by expiry. That heap belongs to a P, not to the whole process. The OS thread then returns to the scheduler and picks up other runnable goroutines. Nothing in the kernel knows a Go-level sleep is in progress. There is no thread parked on it, no `nanosleep` per sleeping goroutine, and no per-timer kernel object. ## What wakes it Go's goroutine scheduler looks at the pending-timer heap as part of its normal work-finding pass. Any timer whose deadline has passed is fired: for a `Sleep`, firing simply marks the sleeping goroutine runnable again and puts it on a run queue. The interesting case is when there is no work at all. Rather than spinning, the thread blocks in the runtime's network poller — `epoll_wait` on Linux, `kqueue` on BSD and macOS. The runtime passes the **earliest pending timer deadline as that wait's timeout**, so the thread wakes either because I/O became ready or because the next timer is due, whichever comes first. One blocking OS call therefore covers arbitrarily many Go timers. That is the whole trick: the runtime folds N Go deadlines into one OS deadline. If a P goes idle while still holding pending timers, the runtime does not let them stall — another P takes responsibility for them, and the runtime's monitoring thread acts as a backstop that can wake a thread when a timer comes due. ## The guarantee you actually get `time.Sleep(d)` pauses for **at least** `d`. It can never return early: it is measured against the monotonic clock, so a system clock adjustment (NTP stepping the wall clock backwards, for instance) does not shorten or lengthen it. It can easily return late, because firing the timer only makes the goroutine *runnable*; the goroutine then waits its turn behind whatever else is queued. On a service that is saturating every P, a 10 ms sleep returning 40 ms later is ordinary, not a bug. Treat sleeps and timers as lower bounds, never as a schedule. ```go start := time.Now() time.Sleep(10 * time.Millisecond) // time.Since(start) >= 10ms, always; how much more depends on load ``` ## The cost model A sleeping goroutine costs its (growable) stack plus one entry in a timer heap. An OS thread blocked in a kernel sleep costs a full thread: a large stack reservation and kernel scheduling state. This is why designs that would be reckless with threads — a hundred thousand connections each with a keep-alive delay, or a backoff layer that holds a goroutine per in-flight retry — are unremarkable in Go. The corollary matters for capacity planning: goroutines parked on timers are almost free in CPU terms, but the moment a large batch of them comes due simultaneously, they all become runnable at once and compete for the same Ps. A thundering herd of expiring backoff timers is a real production shape; jitter on the durations exists to spread that out. ## What to say in an interview "The goroutine is parked and the thread is handed back to the scheduler. The wake-up time goes on a runtime timer heap owned by a P, and the runtime uses the earliest deadline as the timeout of the one blocking wait it already makes for network readiness. So sleeping costs a heap entry, not a thread — and `Sleep` means at least that long, never exactly."
- Can a call to time.Sleep ever return before its duration has elapsed?No. It pauses for at least the requested duration, and it is measured against the monotonic clock, so stepping the system wall clock backwards or forwards does not shorten it. Returning late is normal: firing the timer only makes the goroutine runnable, and it then waits behind other work on a run queue.
- Does each pending sleep cost an OS timer or a kernel object?No. All pending deadlines live in the runtime's own min-heaps. When a thread has no work and is about to block in the network poller, the runtime passes the earliest deadline as that call's timeout, so one blocking OS wait serves every pending Go timer at once.
- Why might a 10 millisecond sleep in a CPU-saturated Go service wake up 50 milliseconds late?The timer's job ends at making the goroutine runnable. Actually running it needs a free scheduling context, and if every P is busy with other goroutines the woken goroutine sits on a run queue. Wake-up latency is a scheduling-delay problem, not a timer-accuracy problem.
One alarm clock holding a list of appointment times, not one alarm clock bought per sleeper.
saying these in an interview costs you the question
- Says time.Sleep blocks an OS thread for the duration
- Thinks the runtime registers one OS timer per Sleep
- Claims Sleep wakes the goroutine at exactly the requested instant
- Believes the kernel, not the Go runtime, tracks the wake-up time
- Assumes changing the system wall clock shortens an in-progress sleep