evio calls epoll and kqueue itself instead of handing work to net
Fast event-loop networking for Go
At a glance
- What is it?
- evio is a small Go event loop for servers that issues epoll and kqueue syscalls directly rather than going through the standard net package. It carries load balancing, UDP, unix sockets, SO_REUSEPORT and one dependency, and the project says outright that it is not a drop-in replacement for net or net/http.
- Who is it for?
- evio earns its place when you are writing a server whose whole job is packets, want one event loop per core without a framework in the middle, and can accept owning the event loop instead of using net. It is the wrong tool for HTTP work already served by net/http, for anyone who wants a transparent swap, or for code that assumes the connection lifecycle events exist on every socket, since UDP gets Data only.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 179 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- tidwall/evio6,037 ★ · -3
| Repository | 2026-09-23 | 2026-11-14 |
|---|---|---|
| tidwall/evio | 6k | 6k |
-3 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Direct syscalls on unix, a net fallback everywhere else
The defining decision is at the bottom of the stack. evio makes direct epoll and kqueue syscalls rather than using the standard Go net package, and works in a similar manner to libuv and libevent. The top level holds evio.go, evio_unix.go, evio_std.go and evio_other.go. On an operating system that has neither epoll nor kqueue, evio falls back to simulating events with the net package. The stated goal is a server framework for Go that performs on par with Redis and Haproxy for packet handling, built as the foundation for Tile38 and a future L7 proxy for Go. The file also warns that evio should not be considered a drop-in replacement for net or net/http.
Connections are evio.Conn values and actions come back from callbacks
You set up events and hand them to the Serve function with the binding addresses. Each connection is an evio.Conn object, passed into the events so your callbacks can tell clients apart. The event set is short: Serving fires when the server is ready to accept connections, Opened and Closed fire when a connection opens and closes, Detach fires when a connection has been detached using the Detach action, Data fires on new data, and Tick fires immediately after startup and again at an interval. Control comes from the second return value of a callback, an evio.Action, which is how you close a single client or shut the whole server down by returning Close or Shutdown from an event. The example below uses only Data and hands the input straight back as output.
One event loop can bind several addresses at once
A single Serve call accepts more than one address, so tcp, udp and unix sockets can share the same loop:
evio.Serve(events, "tcp://192.168.0.10:5000", "unix://socket")The address scheme carries the protocol and the option, which is also how the socket options are passed. The full echo example shows the shape of a server, a Data callback and a panic on a Serve error:
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())
}
}Note what the example does not do: there is no listener to close, no context, and no error path other than the panic.
UDP sockets get the Data event and nothing else
The Serve function can bind UDP addresses, and the cost is stated in two rules. All incoming and outgoing packets are not buffered and are sent individually, so no batching happens behind your back. And the Opened and Closed events are not available for UDP sockets, only the Data event. That second rule is the one that shapes code: anything you wrote around connection identity, connection counts or connection teardown simply has no counterpart on a UDP socket, so a listener shared between tcp and udp has to detect which protocol it is on rather than assume the lifecycle events will arrive. The file documents this as a property of UDP support and does not offer a substitute callback for the missing events.
NumLoops above one hands you the memory model
The events.NumLoops option sets how many loops the server uses. A value of 0 or 1 runs single-threaded, a value above 1 makes the server multithreaded on multi-core machines, and a value of -1 assigns the value equal to runtime.NumProcs() automatically. The warning attached to the multi-loop case is the important sentence: you must take care when synchronizing memory between event callbacks. Nothing in the API serializes your state for you, so a counter or a cache shared between callbacks becomes your problem the moment NumLoops goes above 1. Practically, that means the same binary is single-threaded and safe by default, and becomes the caller's concurrency problem only after an explicit configuration change.
Load balancing is best effort and only exists with NumLoops
The events.LoadBalance option picks how incoming connections are spread across loops, and three values are offered. Random requests that connections are distributed randomly. RoundRobin requests distribution in a round-robin fashion. LeastConnections assigns the next accepted connection to the loop with the least number of active connections. Two constraints come with them: load balancing is always a best effort attempt to distribute connections between loops, and the option is only available when events.NumLoops is set, so setting it on a single loop server does nothing. Nothing here promises a maximum imbalance or an even split, which means a correctness assumption of the form every connection lands on a designated loop would be reading more into this option than it offers.
reuseport=true rides in the address string, and the sample line is wrong
Socket options travel inside the bind address. Providing reuseport=true to an address lets multiple sockets on the same host bind to the same port through SO_REUSEPORT:
evio.Serve(events, "tcp://0.0.0.0:1234?reuseport=true"))That snippet carries an extra closing parenthesis at the end, so it will not compile if you paste it, which is the kind of detail worth knowing before you copy from a document. The feature itself is backed by the repository's only dependency: go.mod requires github.com/kavu/go_reuseport v1.5.0, a single require block in a module file that otherwise declares nothing but the module path and a go 1.15 directive. The README does not show a corrected example anywhere else.
Install with go get, then run one of three example servers
Installing means installing Go and running:
$ go get -u github.com/tidwall/evioThe examples directory holds three runnable programs, a simplified Redis clone, an echo server and a very basic HTTP server:
$ go run examples/http-server/main.go
$ go run examples/redis-server/main.go
$ go run examples/echo-server/main.goPerformance claims are tied to a specific setup rather than left open ended: the published benchmarks were run on an ec2 c4.xlarge instance in single-threaded mode with GOMAXPROC=1 over IPv4 localhost, and the benchmarks directory holds the details. That single-threaded localhost setup is worth noticing, since the NumLoops option above is what changes the picture on a real multi-core host. The code is MIT licensed, the contact listed is Josh Baker, no releases are published, and the last push to master is dated 2026-05-19.
Editorial conclusion
evio earns its place when you are writing a server whose whole job is packets, want one event loop per core without a framework in the middle, and can accept owning the event loop instead of using net. It is the wrong tool for HTTP work already served by net/http, for anyone who wants a transparent swap, or for code that assumes the connection lifecycle events exist on every socket, since UDP gets Data only. Before you build on it, drop the extra closing parenthesis from the README's reuseport snippet before you paste it, and note that go.mod still declares go 1.15 with a single dependency on go_reuseport v1.5.0. The code is MIT licensed, there are no published releases, and the last push to master is dated 2026-05-19.
Frequently asked questions
Is evio a drop-in replacement for Go's net package?
No. The project states plainly that evio should not be considered a drop-in replacement for the standard Go net or net/http packages. It makes direct epoll and kqueue syscalls instead of using the net package, and on operating systems without either it falls back to simulating events with net.
How do I install evio and run a first server?
Install Go and run `go get -u github.com/tidwall/evio`. The smallest example sets only the Data event and serves `tcp://localhost:5000`, returning the incoming bytes as the outgoing bytes, which you can try by connecting with `telnet localhost 5000`.
How do I run evio across multiple CPU cores?
Set events.NumLoops above 1, which makes the server multithreaded on multi-core machines, or set it to -1 to have it equal runtime.NumProcs() automatically, while 0 or 1 keeps the server single-threaded. The file warns that you must take care when synchronizing memory between event callbacks.
Does evio handle UDP and unix sockets as well as tcp?
Yes, the Serve function can bind UDP addresses, and unix sockets can share the same event loop with tcp. For UDP sockets the Opened and Closed events are not available and only the Data event fires, and all incoming and outgoing packets are not buffered and are sent individually.
How does evio spread connections across its loops?
The events.LoadBalance option chooses Random, RoundRobin or LeastConnections, where LeastConnections assigns the next accepted connection to the loop with the least number of active connections. Load balancing is always a best effort, and the option only applies when events.NumLoops is set.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tidwall-evio)Star history chart for your README
[](https://hysenlabs.com/projects/tidwall-evio)Was this useful?