# evio: Event-Loop Networking for Go via Direct epoll and kqueue Syscalls

> evio is a small Go networking library that calls epoll on Linux and kqueue on BSD and macOS directly, bypassing the standard Go net package to reduce goroutine overhead for connection-heavy servers. It was built as the foundation for the Tile38 geospatial database and is not a drop-in replacement for Go's net or net/http packages.

**tidwall/evio** — Fast event-loop networking for Go

- Repository: https://github.com/tidwall/evio
- Stars: 6,040 · Forks: 491
- Language: Go
- License: MIT
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/tidwall-evio

## What evio Solves and Who Should Use It

Go's standard networking model allocates a goroutine for each connection. For servers handling thousands of simultaneous connections, this creates goroutine and memory overhead, even though most connections are idle most of the time. The event-loop pattern addresses this by multiplexing many connections onto a small number of threads using OS-level I/O readiness notifications.

evio implements this pattern in Go. It calls epoll on Linux and kqueue on macOS and BSD directly, rather than using the standard Go net package. The README states the goal: to create a server framework for Go that performs on par with Redis and Haproxy for packet handling.

The library was built as the foundation for Tile38, a geospatial database, and was planned for use in a future L7 proxy. These are the concrete use cases that shaped its design: protocol servers that receive many short messages from many clients simultaneously, where the per-goroutine model is costly.

The README is explicit that evio should not be considered a drop-in replacement for the standard Go net or net/http packages. Teams building HTTP APIs should use net/http. evio is for engineers who understand the event-loop model and need that specific performance profile. The library is compact: the main source files are evio.go (the public API and loop coordination), evio_unix.go (the epoll and kqueue implementations), evio_other.go (the net package fallback for non-Unix platforms), and evio_std.go (the standard-library simulation of events).

## Installing and Starting a Server

Install evio with go get:

```sh
$ go get -u github.com/tidwall/evio
```

A minimal echo server that binds to port 5000 shows the core API:

```go
package main

import "github.com/tidwall/evio"

func main() {
	var events evio.Events
	events.Data = func(c evio.Conn, in []byte) (out []byte, action evio.Action) {
		out = in
		return
	}
	if err := evio.Serve(events, "tcp://localhost:5000"); err != nil {
		panic(err.Error())
	}
}
```

The entire setup is five lines of logic. evio.Serve starts the event loop with the given events and address. The Data callback fires when data arrives from a client; returning the input bytes as out sends them back. To test, connect with:

```sh
$ telnet localhost 5000
```

To run one of the included examples:

```sh
$ go run examples/http-server/main.go
$ go run examples/redis-server/main.go
$ go run examples/echo-server/main.go
```

The examples directory includes a simplified Redis clone, an echo server, and a basic HTTP server. The go.mod file lists go 1.15 as the minimum version and github.com/kavu/go_reuseport as the only external dependency. This makes evio unusually self-contained for a networking library; the SO_REUSEPORT implementation is the one place where an external package is needed, because kernel socket option handling is platform-specific enough to benefit from a dedicated package.

## Events: the API Surface

The evio.Events struct is the complete API. You populate only the callbacks you need and pass the struct to evio.Serve. The available events are:

Serving fires when the server is ready to accept new connections.

Opened fires when a connection is opened. It is not available for UDP sockets.

Closed fires when a connection is closed. Also unavailable for UDP.

Detach fires when a connection has been detached using the Detach return action.

Data fires when the server receives new data from a connection. This is the primary callback for any request-response server.

Tick fires immediately after the server starts and then fires again after a specified interval. The Tick callback returns a delay duration that controls the interval for the next tick. This is useful for background tasks that need to run periodically, such as expiring stale connections.

A server can bind to multiple addresses and share the same event loop:

```go
evio.Serve(events, "tcp://192.168.0.10:5000", "unix://socket")
```

The Tick callback pattern from the README:

```go
events.Tick = func() (delay time.Duration, action Action){
	log.Printf("tick")
	delay = time.Second
	return
}
```

## Multithreading, Load Balancing, and SO_REUSEPORT

By default, evio runs a single event loop on a single thread. For multi-core machines, set events.NumLoops to run multiple loops:

- Value 0 or 1: single-threaded.
- Value greater than 1: that many event loops, effectively multithreaded.
- Value -1: automatically assigns equal to runtime.NumProcs().

When running multiple loops, memory access between event callbacks must be synchronized manually. Each loop runs in its own goroutine, and callbacks from different clients may run concurrently across loops.

The events.LoadBalance option controls how new connections are distributed across loops. The three modes are Random (connections are randomly distributed), RoundRobin (round-robin distribution), and LeastConnections (new connections go to the loop with the fewest active connections).

For SO_REUSEPORT, pass the option as a query parameter in the address string:

```go
evio.Serve(events, "tcp://0.0.0.0:1234?reuseport=true")
```

SO_REUSEPORT allows multiple sockets on the same host to bind to the same port. This is useful when running multiple processes or when the OS can distribute incoming connections across sockets.

## UDP Support and Non-epoll Fallback

evio accepts UDP addresses in evio.Serve alongside TCP and Unix socket addresses. The UDP behavior is more limited than TCP: incoming and outgoing packets are not buffered and are sent individually. The Opened and Closed events are not available for UDP sockets, only the Data event.

On operating systems that do not support epoll or kqueue, such as Windows, evio falls back to simulating events using the standard Go net package. The fallback is in evio_std.go. The performance advantages of the direct syscall path apply only on Linux (epoll) and macOS and BSD (kqueue); on other platforms, evio runs at roughly the performance of the standard net package.

evio_unix.go contains the epoll and kqueue implementations. evio_other.go provides the non-Unix fallback path. The internal/ directory contains platform-specific helpers.

For benchmarks, the README notes they were run on an EC2 c4.xlarge instance in single-threaded mode (GOMAXPROC=1) over IPv4 localhost. The benchmarks/ directory contains the benchmark code. No benchmark numbers are cited in the README itself. The benchmark setup is noted as a reference point, not a guarantee; actual performance depends on the specifics of the workload, the number of loops, and the load balancing mode.

For UDP, the behavior differs from TCP in one important practical way: the server cannot use the Opened event to initialize per-connection state before the first message arrives, because no Opened event fires for UDP. All per-client state must be initialized on the first Data event for that client's source address.

## evio vs the Standard Go net Package

The standard Go net package abstracts network connections behind net.Conn, goroutines, and channels. Each accepted connection runs in its own goroutine, which Go schedules efficiently but not free of cost. For servers that hold many thousands of idle connections simultaneously, the goroutine-per-connection model consumes more memory and scheduler time than event-loop alternatives.

evio eliminates the goroutine-per-connection model by multiplexing connections onto a fixed number of event loop goroutines using epoll or kqueue. The cost is a more constrained programming model: you work with callbacks and byte slices rather than net.Conn and io.Reader, and you manage the protocol state machine yourself.

Liuv and libevent are C libraries that use the same event-loop approach and are the most widely known examples of the pattern in the C ecosystem. The README names them as references for what evio does. The difference from evio is language: libuv and libevent are C libraries, while evio runs in Go and produces a single Go binary.

For most Go web services, net/http and its ecosystem are the correct choice. evio is appropriate when the connection count is high enough that the goroutine overhead becomes measurable, or when building a custom TCP protocol server where you need maximum control over connection handling.

## Conclusion

evio is the right choice for Go servers that need to handle a very large number of simultaneous connections with minimal goroutine overhead, and where the application protocol is simple enough to implement in callbacks without the abstraction of net.Conn. The README explicitly says evio is not a drop-in replacement for the standard Go net or net/http packages. Teams building standard HTTP APIs or using gRPC should use those libraries directly. Before using evio in a multi-loop configuration, account for the manual synchronization requirement between event callbacks.

## FAQ

### Can evio replace the standard Go net package?

The README explicitly states that evio should not be considered a drop-in replacement for the standard Go net or net/http packages. evio uses a callback-based event-loop API, not the net.Conn interface. It targets servers that need direct epoll and kqueue control for high-connection-count workloads.

### Does evio work on Windows?

On operating systems that do not support epoll or kqueue, including Windows, evio falls back to simulating events using the standard Go net package. The direct performance advantages of the event loop apply only on Linux (epoll) and macOS and BSD (kqueue).

### How do I run multiple event loops with evio?

Set events.NumLoops to a value greater than 1 to run multiple loops for multi-core machines. Setting it to -1 automatically assigns a value equal to runtime.NumProcs(). When using multiple loops, memory access between event callbacks must be synchronized manually.

## Sources

- [Issues](https://github.com/tidwall/evio/issues)
- [License: MIT](https://github.com/tidwall/evio/blob/master/LICENSE)
- [README](https://github.com/tidwall/evio/blob/master/README.md)
- [tidwall/evio on GitHub](https://github.com/tidwall/evio)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tidwall-evio
