Skip to content

netpoll ​

Before You Start

  • Applies to: Go 1.21+ (examples verified with Go 1.22) · Prerequisites: Channel · Read time: about 10 min

Go's netpoll is a core I/O multiplexing mechanism implemented by the Go runtime. It enables Go to implement high-performance network services with a concise synchronous programming model (one Goroutine per connection). Its essence is the deep integration of operating system-level I/O multiplexing system calls (such as Linux's epoll) with Go's Goroutine scheduler.

Comparison between Netpoll and Native Go Net ​

1. Number of Goroutines ​

  • Go Net: One connection per Goroutine.
  • NetPoll: Multiple connections under one Goroutine.

2. Goroutine Context Switching/Scheduling Pressure ​

  • Go Net: High switching pressure under high concurrency. Because Go Net has one connection per goroutine, frequent switching occurs when there are many goroutines.
  • NetPoll: Low switching pressure under high concurrency. Because NetPoll shares one goroutine among multiple connections, there are fewer switches.

3. Memory Consumption ​

  • Go Net: Memory consumption is positively correlated with the number of connections. High concurrency may cause memory pressure due to Goroutine explosion.
  • NetPoll: Pre-allocates buffers and creates a buffer pool.

4. Trigger Mode ​

  • Go Net: Uses Edge Triggered (ET). Reads all data at once, simple implementation.
  • NetPoll: Uses Level Triggered (LT). No need to pre-allocate large memory as data buffers.

5. Support for Kernel and User Layer Sharing Buffer Pool (One Less Copy) ​

  • Go Net: Not supported.
  • NetPoll: Supported. Manages a Buffer pool directly handed to users, requiring one less copy.

6. Applicable Scenarios ​

  • Go Net: Low concurrency, simple scenarios.
  • NetPoll: High concurrency, low latency scenarios.

Netpoll Design Concepts ​

1. Basic Architecture ​

The native network library is developed based on epoll LT mode. The basic architecture is shown in the figure below:

netpoll basic architecture

2. Usage of Goroutines ​

There is a poll object pool. Each poll object has an epoll and is separately associated with a goroutine. The number of goroutines is consistent with the number of poll objects, and each poll object can listen to multiple file descriptors (fd).

3. I/O Read/Write Logic ​

  • Each poll object starts a goroutine to continuously poll readable/writable events on the current epoll.
  • Each epoll is associated with multiple fds, and each fd is associated with a Buffer.
  • When a readable event is heard from an fd, data is read into the Buffer.
  • Continuously poll and process data in the Buffer. When data reading is complete, notify the subsequent processing logic's goroutine pool GoPoll to execute.
  • Interaction with the kernel through system calls is entirely controlled by netpoll. User-level reads and writes to Conn are just operating on the shared Buffer.

4. Advantages and Disadvantages ​

Advantages: Stronger support for high concurrency capabilities.

  1. Polling processes data in the Buffer, so there's no situation where data doesn't get processed.
  2. One goroutine corresponds to multiple connections. Even with many connections, the overhead of resource scheduling and switching is not significant.
  3. Supports user and kernel buffer sharing, reducing one data copy operation and improving efficiency.

Disadvantages: Requires more memory.

Complete Example ​

The program below runs as-is: copy it into a file and execute go run main.go, or paste it into the Go Playground.

go
package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
	// A tiny echo server on a random local port
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	defer ln.Close()

	go func() {
		conn, err := ln.Accept()
		if err != nil {
			return
		}
		defer conn.Close()
		io.Copy(conn, conn) // echo
	}()

	conn, err := net.Dial("tcp", ln.Addr().String())
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	if _, err := conn.Write([]byte("ping")); err != nil {
		panic(err)
	}
	buf := make([]byte, 4)
	if _, err := io.ReadFull(conn, buf); err != nil {
		panic(err)
	}
	fmt.Println(string(buf))
}

Expected output:

text
ping

Common Pitfalls ​

  • Blocked I/O parks the goroutine, not the thread. That is why one goroutine per connection scales.
  • Regular files do not use netpoll. File reads are handled by blocking syscalls, so a huge file copy can block a thread.
  • Always set deadlines in servers. SetReadDeadline prevents slow clients from holding goroutines forever.
  • Buffer your I/O. Wrap connections in bufio.Reader/bufio.Writer to reduce syscalls.
  • Close connections on all paths. defer conn.Close() in every goroutine; leaked sockets exhaust file descriptors.

Change Log ​

  • 2026-10-14: Added "Before You Start" notes, a complete runnable example with expected output, common pitfalls, and this change log.

Golang by www.golangdev.cn · Adapted in part from Golang 中文学习文档 (MIT, © 寒江蓑笠翁)