benchmarks
Benchmarks
One core per server, loopback, medians of five interleaved rounds on a 4-vCPU virtual machine; the thin line under a bar runs from the lowest round to the highest. giong is not faster than everything at everything: the charts say where the design pays off and where it does not.
System calls per request
/json, 50 connections · lower is better
The structural difference, and the most robust result here. On the small response it buys little, because a request costs about 4 µs of kernel time whichever way it is delivered. On 8 KB responses, where a whole batch leaves in one call, it is worth almost half the CPU: 16.0 µs per request against 27.9 for raw Tokio.
5,000 connections: CPU per request
/json, µs of user plus kernel time · lower is better
giong is 11% below hand-written epoll and 16% below tuned Monoio, with the lowest p99 (26 ms; epoll 32, Monoio 44, raw Tokio 54). The likely reason is the provided buffer ring: receive buffers are reused while still warm in cache. At 50 and 1,000 connections giong, tuned Monoio and epoll are level.
HTTPS on one core
requests per second, TLS 1.3, the same rustls on both sides · higher is better
TLS costs giong 4% on /json and 14% on 8 KB responses. Handshakes are level: a handshake is almost all cryptography, and both sides use the same library for it. Most of the /users factor is there without TLS.
One connection: median latency
/json, microseconds · lower is better
With one connection every request finds a sleeping server. spin_before_park(200 µs) keeps the worker watching its ring: 47.9k requests/s instead of 32.4k. It costs CPU, 20.5 instead of 13.6 µs per request, and the worker keeps its core busy between requests. Without it giong is the same as the others.
File reads on one core
4 KiB at a random offset, one task, operations per second · higher is better
5.5 times Tokio, where every read is a hand-off to another thread and back; on giong it is one entry in a batch. A blocking std call is 28% faster for cached data. What giong buys is a worker that never waits when the data is not cached, which this benchmark does not measure.
Small responses at 50 connections: a tie
/json, requests per second
giong, tuned Monoio, raw Tokio and epoll are within noise: the kernel's TCP stack, not the runtime, is most of the cost, and wrk is the limit. giong-http reads higher and we do not know why. It costs 7% more CPU per request, as checking every header should, and keeps its core busier. Treat that row as a measurement of the setup.
What these numbers do not show
- One machine. Everything is loopback inside a 4-vCPU virtual machine, where the load generator competes for the same few cores. The 14-core run in the README (1 to 10 workers, peak 1.50M requests/s) is loopback too, on a desktop under WSL2.
- Not measured: a physical network, scaling on bare metal, and disks. The file benchmark reads cached data only.
- 8 KB responses: giong and Monoio serve 63–64k requests/s, everything else 35–37k. Most of that gap comes from the virtual machine. On physical hardware expect a smaller one.
/computeis the same for everyone, as it should be: the time goes into the function, not the runtime.- Two cores are measured and are not a ranking. Two
wrkthreads are as likely to be the limit as the server. - Monoio is compared tuned, with
DEFER_TASKRUNpassed in. Its default ring is about 20% slower on the small response; comparing against that would flatter giong.
compare
Compared with Monoio and Glommio
The other io_uring runtimes for Rust, as they stood when the benchmarks ran. Monoio and Glommio are mature projects with wider ecosystems; this table is about how each one is built, not a ranking.
| giong | Monoio 0.2.4 | Glommio 0.9.0 | |
|---|---|---|---|
| Platforms | Linux ≥ 6.0 | Linux ≥ 5.6, with an epoll/kqueue fallback (runs on macOS) | Linux ≥ 5.8, needs a raised memlock limit |
| Receiving | multishot, kernel-provided buffers | one submission per read, caller's buffer | direct recv, io_uring to wait |
| Accepting | multishot | one submission per accept | one submission per accept |
| Buffer API | ownership passing | ownership passing | borrowed (AsyncRead / AsyncWrite) |
| Timeout around a read | loses no data | needs the separate cancelable_* API | — |
| Ring defaults | DEFER_TASKRUN on | no flags; pass your own builder | — |
| Busy-poll option | yes | no | yes |
| Multi-core | built-in builder with graceful draining and signals | #[monoio::main(threads = N)] | executor pools placed by CPU topology |
| Scheduling | FIFO; new work placed on the least busy worker on request | FIFO | weighted queues with latency classes |
| I/O | TCP, UDP, Unix sockets, files | TCP, UDP, Unix sockets, files | TCP, UDP, Unix sockets, Direct I/O files |
| Ecosystem | its own HTTP/1.1 server and client, web framework, TLS for both sides, DNS, PostgreSQL, MySQL and Redis clients | TLS, HTTP, codecs, Tokio compatibility layer | channels, semaphores, rate control |
reproduce
Run them yourself
Each script builds the servers, runs the load generator and writes its summary to bench/results/. Run them on a quiet Linux machine; they take minutes, not seconds.