Go Internals Part 2: Blocking Syscalls and the Network Poller
When a goroutine makes a blocking syscall, the runtime can’t let that stall the M — that would waste a thread and starve the P’s run queue.
Handoff
Before entering a blocking syscall, the runtime detaches the P from the current M and hands it off to another M (or spawns a new one). The original M blocks in the kernel, but the P keeps scheduling other goroutines.
When the syscall returns, the original M tries to re-acquire a P. If none are free, the goroutine is put back on the global run queue and the M parks itself.
The Network Poller
Network I/O is different. Go uses a non-blocking I/O model backed by epoll (Linux), kqueue (macOS), or IOCP (Windows). When a goroutine reads from a net.Conn and no data is ready:
- Goroutine is parked and registered with the netpoller
- M is freed to run other goroutines
- When data arrives, the netpoller wakes the goroutine and puts it back on a P’s run queue
// From user code this is invisible — looks like blocking I/Oconn.Read(buf) // goroutine parks here if no data, no thread wastedThis is why Go can handle tens of thousands of concurrent connections with a small thread pool.
Next: memory allocation and the GC.
← Back to blog