Under The Hood
backend go Go 1.21+ (netpoll)

Go Netpoller: OS epoll/kqueue Integration Without Blocking Threads

Last updated
Prerequisites:
Go goroutines and network I/O
Understanding of non-blocking I/O and OS event loops
  • go
  • netpoller
  • epoll
  • kqueue
  • io
  • goroutine
  • network

Read at your depth

The practical view

Go's netpoller lets goroutines do network I/O without each blocking an OS thread. When a goroutine reads from a socket with no data yet, it's parked; the netpoller (built on epoll on Linux, kqueue on BSD/macOS, IOCP on Windows) watches all sockets at once. When data arrives, the netpoller wakes the goroutine. This is why a Go server can handle tens of thousands of concurrent connections with a small number of threads. The standard library net package uses this automatically — you don't write epoll yourself; goroutines + netpoller give you the benefit of an event loop with the ergonomics of blocking code.

The same idea in other frameworks

react equivalent
N/A — Go runtime concern.

Legacy vs modern

Thread-per-connection vs goroutine + netpoller

A thread per connection caps concurrency and wastes stack memory; goroutines parked on the netpoller let one thread serve thousands of connections.

before → after
Thread-per-connection (other languages)
while (true) {
  conn = accept();
  new Thread(() -> handle(conn)).start(); // ❌ OLD: thread per connection
}
Goroutine per connection on the netpoller
ln, _ := net.Listen("tcp", ":8080")
for {
  conn, _ := ln.Accept()
  go handle(conn) // ✅ NEW: goroutine, parked on netpoller while idle
}

Interview gotchas

Context

This is the core netpoller question — interviewers want the park/register/wake cycle and how the runtime separates goroutine blocking from thread blocking.

The mechanical answer

When a goroutine calls conn.Read and no data is available, the runtime's pollDesc registers the fd with the netpoller (epoll_ctl) and calls gopark on the goroutine. gopark suspends the goroutine and returns the OS thread to the scheduler, which immediately runs another goroutine on that thread. The netpoller has its own polling loop (epoll_wait) that blocks at the OS level on behalf of ALL goroutines. When the fd becomes readable, epoll returns the event, and the netpoller unparks the corresponding goroutine (ready), which resumes the Read call and copies the data. So a blocked goroutine consumes no thread, no stack growth beyond its current size, and no OS scheduling slot — the thread count stays near GOMAXPROCS while the connection count can be in the millions.

Trap

The naive answer is 'a blocked goroutine blocks its thread'. It parks on the netpoller and the thread is reused. Another trap: thinking this applies to ALL I/O — it applies to the netpoller-backed fd types (sockets, pipes); truly blocking syscalls (via cgo or a wrapped blocking call) and file I/O on some platforms still occupy threads (the runtime may add threads for them). Candidates who describe gopark and the poll thread show real understanding.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.