Library / SDK
talostrading/sonic avatar
talostrading/sonic

Sonic runs many connections on one goroutine and one thread

Sonic is a Go library for network and I/O programming that provides developers with a consistent asynchronous model, with a focus on achieving the lowest possible latency and jitter in Go.

793 stars29 forksGoMIT

At a glance

What is it?
Sonic is a Go library that replaces the standard `net` package with an IO object that services many connections from a single goroutine, so a program can be pinned to one thread on an isolated core with no channels, no mutexes and no scheduler work in the way. It suits people writing latency sensitive protocols, exchange connections and custom binary wire formats.
Who is it for?
Adopt Sonic when your Go program talks to several connections at once and the scheduler, the allocation and the syscalls are what stand between you and a stable latency figure, since the library exists to move that work out of goroutine scheduling and into one hot loop. Do not adopt it for general server work, because the model deliberately removes the concurrency model most Go code relies on, and it currently supports only Unix based systems.
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 44 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 October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Many connections, one goroutine

The README states the design directly. Sonic aims to make it easy to write network protocols, websocket, http2 or a custom exchange binary format, on a series of bytestreams, and then to use those bytestreams through multiple connections running in a single thread and goroutine. It is described as an alternative to the `net` package, and the difference it removes is the need to use multiple goroutines to handle multiple connections and reads and writes in the same process. The consequences are the three benefits the README lists. You need no synchronization primitives, since channels and mutexes are not involved when several connections are handled in the same goroutine. The Go scheduler has no work to do, so it cannot slow the program down. And a latency sensitive program can run in a hot loop pinned to a thread on an isolated core. The trade-off is implied rather than stated, and it is large: a single goroutine means one slow callback blocks every connection.

The IO object and the async read model

The README's opening example shows the shape of the API. You create an IO object that can execute asynchronous operations on the current goroutine, defer its close, then dial connections and schedule reads on them:

go
func main() {
    // Create an IO object which can execute asynchronous operations on the
    // current goroutine.
    ioc := sonic.MustIO()
    defer ioc.Close()

    // Create 10 connections. Each connection reads a message into it's
    // buffer and then closes.
    for i := 0; i < 10; i++ {
        conn, _ := sonic.Dial(ioc, "tcp", "localhost:8080")
        
        b := make([]byte, 128)
        conn.AsyncRead(b, func(err error, n int) {
            if err != nil {
                fmt.Printf("could not read from %d err=%v\n", i, err)
            } else {
                b = b[:n]
                fmt.Println("got=", string(b))
                conn.Close()
            }
        })
    }

    // Execute all pending reads scheduled in the for-loop, then exit.
    ioc.RunPending()
}

The two calls to note are `AsyncRead`, which schedules a read with a completion callback, and `RunPending`, which executes the reads that have been scheduled. Nothing runs until the pending queue is processed, and then the loop runs the callbacks.

UDP multicast without allocating or copying

The multicast peer is called out separately because it is a full featured peer for both IPv4 and IPv6, and because it behaves differently from the websocket client in one specific way. It can read and write to a multicast group, join a group with source IP and network interface filtering, and control group membership at runtime by blocking and unblocking source IPs. The README states that this peer, unlike the websocket client, does not allocate and copy any data in any of its functions. That is a strong claim and it is the reason the peer exists in this form. It also allows the read buffer to be changed after a read has been scheduled, so a read scheduled against one buffer can be pointed at a different one before it happens:

go
var (
    b1, b2 []byte
)
peer.AsyncRead(b1, func(...) { ... }) // schedule an asynchronous read in b1
// ... some other code here
peer.SetAsyncReadBuffer(b2) // make the previously scheduled asynchronous read use b2 instead of b1

That is what makes several peers share one large read buffer, each reading into a different slice of it and processing the previous chunk while the next arrives.

FIFO buffers that avoid the copy

The README spends a long passage on the problem that the buffer types solve, and the argument is worth following. Write a protocol with a fixed size header and a variable payload, and read it from TCP. The first option is to read the header, work out the payload size, and then read the payload, which needs two syscalls per message. The second option is to read as much as possible and parse the bytes, which needs one syscall for n messages, but breaks when the last message is incomplete, because the bytes that did not fit have to be moved to the front of the buffer, overwriting the payloads already processed. The naive fix is to copy those bytes, and a memcpy of a kilobyte or more is expensive. Sonic offers two FIFO types with the same API instead. `Claim(n) []byte` claims at most n bytes to read into, `Commit(n) int` commits at most n previously claimed bytes, and `Consume(n) int` consumes at most n previously committed bytes. The websocket path does copy, in `byte_buffer.go` and the websocket codec, which the README is candid about.

The mirrored buffer maps the same memory twice

The first of the two buffer types is the mirrored buffer, which works for both TCP and UDP and offers contiguous byte slices in FIFO order without caring about wrapping. The implementation is in `bytes/mirrored_buffer.go`, and the README states the limitations plainly. The buffer size must be a multiple of the system's page size, and the system must expose a shared memory filesystem such as `/dev/shm`. The mechanism is that the buffer creates the underlying byte slice of size n, where n is a multiple of the page size, and maps it twice, contiguously, in the process's virtual address space with `mmap`. There are n physical bytes backing the slice and n times 2 virtual bytes. The consequence for an adopter is that the shared memory requirement is not a detail to skip, since the buffer does not work on a system without `/dev/shm`.

Examples that look like the workloads it targets

The `examples/` directory is the best description of the intended use. There is `timer`, which the README names as a good starting point, alongside `echo`, `tcp`, `udp`, `websocket`, `file`, `post`, `multicast`, `sequencing`, `bip_buffer`, `async_adapter`, `many-iocs` and `latency-test`. Three of them name the world Sonic came from. `binance` and `okex` are exchange connections, which is the custom binary exchange protocol the description mentions, and `iperf` and `ws-latency` are measurement tools. The point of the example set is that it covers the shapes of traffic that make a single threaded design pay off, from a timer loop to a websocket client to an exchange feed. The `net/` and `other/` directories at the repository root, along with `codec/`, `sonicopts/`, `sonicerrors/` and `internal/`, show the same split in the library itself, with protocol codecs, option and error types, and platform specific socket code such as `socket_bsd.go` and `socket_linux.go`.

Building, testing and the security tooling

The Makefile shows the working vocabulary of the project. `make` runs lint and then the build script, `make linux` does the same for Linux, and `make fmt` runs gofmt with the simplify flag and goimports over the tree. The lint step is `golangci-lint` with four jobs, a fast mode and a five minute timeout, and there is a separate `gosec` target that emits SARIF and excludes the examples, the stress tests and the docs. The test target is unusual enough to be worth reading, because it sets `GODEBUG=asyncpreemptoff=1` and runs the packages serially with `-p 1`, excluding the examples and the websocket performance tests. The benchmark target uses the same settings with the benchmark flag. That environment variable is the tell. With asynchronous preemption off, the Go runtime will not interrupt a goroutine at an arbitrary point, which is what makes a hot loop's timing meaningful, and the same reasoning explains why Sonic insists on running its connections in one goroutine.

Unix only, and unstable until v1.0.0

Two constraints frame any decision about this library. The first is platform. Sonic currently supports only Unix based systems, BSD, macOS and Linux, so it is not an option for a Windows service, and the socket code is split into `socket_bsd.go` and `socket_linux.go` accordingly. The second is maturity. The first line of the README says work in progress and expects breaking changes until v1.0.0, and the release history backs that up, with v0.9.0, v0.9.1 and v0.9.2 all published within November 2024 and the last push to the repository on 2026-09-09. The licence is MIT, and the module requires Go 1.24.1, with a dependency set kept deliberately light, including an HDR histogram for latency measurement, a profiling library, and the byte buffer pool.

Editorial conclusion

Adopt Sonic when your Go program talks to several connections at once and the scheduler, the allocation and the syscalls are what stand between you and a stable latency figure, since the library exists to move that work out of goroutine scheduling and into one hot loop. Do not adopt it for general server work, because the model deliberately removes the concurrency model most Go code relies on, and it currently supports only Unix based systems. Verify first that your protocol can live with a single threaded event loop, and note that the repository is marked work in progress with breaking changes expected until v1.0.0, so the API should be treated as unstable.

Frequently asked questions

What problem does Sonic solve in Go?

It removes the need to use multiple goroutines to handle multiple connections and reads and writes in the same process. A single goroutine and thread reads and writes from several connections, which removes synchronization primitives, keeps the Go scheduler out of the way, and lets a latency sensitive program run pinned to an isolated core.

Which operating systems does Sonic support?

Only Unix based systems, BSD, macOS and Linux. The socket implementation is split into `socket_bsd.go` and `socket_linux.go` in the repository, so Windows is not currently supported.

What are the Claim, Commit and Consume methods for?

They are the zero-copy FIFO API over a byte slice. `Claim(n)` claims at most n bytes to read into, `Commit(n)` commits at most n previously claimed bytes, and `Consume(n)` consumes at most n previously committed bytes, so a partially received message does not have to be copied to the front of the buffer.

Is Sonic ready for production use?

The README marks it as work in progress and expects breaking changes until v1.0.0. The listed releases are v0.9.0, v0.9.1 and v0.9.2 from November 2024, and the last push to the repository was on 2026-09-09, so the API should be treated as unstable.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. talostrading/sonic on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/talostrading-sonic.svg)](https://hysenlabs.com/projects/talostrading-sonic)

Was this useful?