v0.2.1Linux 6.0+Rust 1.80+MIT OR Apache-2.0

One syscall per tick, not per operation.

giong is a thread-per-core async runtime for Rust on Linux, built directly on io_uring. Every read, write, accept and close that one turn of the event loop produces enters the kernel in a single io_uring_enter.

cargo add giong
One turn of the event loop, drawn to explain the mechanism. Measured on /json at 50 connections: 0.03 system calls per request for giong, 2.05 for raw Tokio. bench/syscalls.sh

Named after Thánh Gióng, the child of Vietnamese legend who lay silent for three years and then rose a giant. giong costs next to nothing while idle and uses every core the moment load arrives.

5,400lines
of source in the runtime, with a comment on every unsafe block
3
dependencies: io-uring, libc, socket2
169tests
passing on three io_uring setups, clean under ThreadSanitizer and valgrind
5.6MB
resident memory after the 50-connection benchmark

why

Where the design pays off

giong commits to io_uring and to one thread per core, and to nothing else. These are the six things that buys.

io_uring_enter · DEFER_TASKRUN · SINGLE_ISSUER

One syscall per tick

Every read, write, accept, close and cancellation a turn of the loop produces enters the kernel together, and every ready completion comes back in the same call. A request costs about 0.03 system calls; an epoll-based server needs 2.

multishot accept · multishot recv · provided buffers

Idle connections hold no buffer

One submission lasts the life of a listener or connection. The kernel picks a buffer only when data arrives, so it is still warm in cache. At 5,000 connections giong spends the least CPU per request of the runtimes measured.

tests/cancel.rs

Cancel safe by construction

Drop an I/O future mid-flight and its buffer moves to the driver until the kernel confirms the cancel. A timeout around recv, recv_from or accept loses no data, datagram or connection.

Builder::spin_before_park

A latency dial

Let a worker watch its ring for a while before it sleeps. On a single connection that is 19 µs median instead of 28 and 48% more requests. It costs CPU, so it is off by default.

giong::fs

Files on the same ring

Open, read, write, fsync, statx, rename and remove are entries in the worker's ring, next to the network. No pool of blocking threads. A file has no cursor: every read and write names its offset.

!Send · Rc · RefCell

Shared nothing

One thread per core, and a task never changes thread. Futures need not be Send; Rc and RefCell just work. No locks on the hot path.

numbers

Measured, with the script next to every number

One core per server, loopback, medians of five interleaved rounds on a 4-vCPU virtual machine. 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

  1. giong0.03
  2. giong-http0.03
  3. Monoio (tuned)0.04
  4. Hand-written epoll2.03
  5. Raw Tokio2.05
  6. Glommio2.10

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.

bench/syscalls.sh

One connection: median latency

/json, microseconds · lower is better

  1. giong + spin19 µs
  2. giong28 µs
  3. Monoio (tuned)28 µs
  4. Raw Tokio29 µs
  5. Hand-written epoll29 µs
  6. giong-http30 µs
  7. Glommio33 µs

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.

bench/scale.sh

5,000 connections: CPU per request

/json, microseconds of user plus kernel time · lower is better

  1. giong4.42 µs
  2. Hand-written epoll4.99 µs
  3. giong + spin5.12 µs
  4. Monoio (tuned)5.25 µs
  5. giong-http5.37 µs
  6. Raw Tokio5.74 µs
  7. Glommio8.29 µs

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.

bench/scale.sh

HTTPS on one core

requests per second, TLS 1.3, the same rustls on both sides, thin line from lowest to highest round · higher is better

  1. /json, kept-alive
  2. giong-http + giong-tls144.8k
  3. Axum + rustls81.0k
  4. /users, 8 KB, kept-alive
  5. giong-http + giong-tls47.8k
  6. Axum + rustls19.2k
  7. a new handshake per request
  8. giong-http + giong-tls7.1k
  9. Axum + rustls7.3k

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.

bench/tls.sh

File reads on one core

4 KiB at a random offset, one task, operations per second, thin line from lowest to highest round · higher is better

  1. std (blocking)1.40M
  2. giong1.01M
  3. Tokio, one core184k
  4. Tokio, 4 cores33k

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.

bench/fs.sh

Small responses at 50 connections: a tie

/json, requests per second, thin line from lowest to highest round

  1. giong-http195.7k
  2. Monoio (tuned)176.0k
  3. Raw Tokio174.1k
  4. giong173.5k
  5. Hand-written epoll170.9k
  6. Axum161.0k
  7. Bun147.7k
  8. Glommio143.9k
  9. Node.js39.5k

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.

bench/run.sh

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.
  • /compute is 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 wrk threads are as likely to be the limit as the server.
  • Monoio is compared tuned, with DEFER_TASKRUN passed in. Its default ring is about 20% slower on the small response; comparing against that would flatter giong.

Full tables, method and raw output: README · bench/results/

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 0.2Monoio 0.2.4Glommio 0.9.0
PlatformsLinux ≥ 6.0Linux ≥ 5.6, with an epoll/kqueue fallback (runs on macOS)Linux ≥ 5.8, needs a raised memlock limit
Receivingmultishot, kernel-provided buffersone submission per read, caller's bufferdirect recv, io_uring to wait
Acceptingmultishotone submission per acceptone submission per accept
Buffer APIownership passingownership passingborrowed (AsyncRead / AsyncWrite)
Timeout around a readloses no dataneeds the separate cancelable_* API—
Ring defaultsDEFER_TASKRUN onno flags; pass your own builder—
Busy-poll optionyesnoyes
Multi-corebuilt-in builder with graceful draining and signals#[monoio::main(threads = N)]executor pools placed by CPU topology
SchedulingFIFOFIFOweighted queues with latency classes
I/OTCP, UDP, Unix sockets, filesTCP, UDP, Unix sockets, filesTCP, UDP, Unix sockets, Direct I/O files
Ecosystemits own HTTP/1.1 server and server-side TLS; nothing elseTLS, HTTP, codecs, Tokio compatibility layerchannels, semaphores, rate control
Size~5,400 lines, plus ~2,250 for HTTP and TLS~25,000 lines~40,500 lines

Source: README, “Compared with Monoio and Glommio”

code

What the code looks like

Every worker binds its own SO_REUSEPORT listener on the same port and the kernel spreads connections across cores. Buffers go to the kernel by value and come back with the result.

HTTP on every core · giong-http

use std::time::Duration;
use giong::net::TcpListener;
use giong_http::{Request, Response, Server};

async fn handler(req: Request) -> Response {
    match (req.method(), req.path()) {
        ("GET", "/health") => Response::text("ok"),
        ("POST", "/echo") => Response::new(200).body(req.body().to_vec()),
        _ => Response::new(404),
    }
}

fn main() -> std::io::Result<()> {
    giong::Builder::new()
        .shutdown_on_signals(true)                  // Ctrl-C / SIGTERM stop it gracefully
        .shutdown_grace(Duration::from_secs(10))    // requests in progress are answered
        .build()?
        .run(|_worker| async {
            // Every worker binds its own SO_REUSEPORT listener on the same port;
            // the kernel spreads connections across cores.
            let listener = TcpListener::bind("0.0.0.0:8080").expect("bind");
            Server::new().serve(listener, handler).await;
        })
}

One listener per worker; requests in progress are answered before a worker stops.

HTTPS · giong-http + giong-tls

let acceptor = giong_tls::TlsAcceptor::from_pem(
    &std::fs::read("cert.pem")?,
    &std::fs::read("key.pem")?,
)?;

// ... in each worker:
let listener = TcpListener::bind("0.0.0.0:8443").expect("bind");
Server::new().serve_tls(listener, acceptor.clone(), handler).await;

Turn on the tls feature of giong-http. Handlers, limits, timeouts and graceful shutdown are the same as for plain HTTP.

The bare runtime · giong

use giong::net::TcpListener;

fn main() -> std::io::Result<()> {
    giong::Builder::new()
        .shutdown_on_signals(true)
        .build()?
        .run(|_worker| async {
            let listener = TcpListener::bind("0.0.0.0:8080").expect("bind");
            while let Some(Ok(stream)) = giong::until_shutdown(listener.accept()).await {
                giong::spawn(async move {
                    loop {
                        let Ok(data) = stream.recv().await else { return };
                        if data.is_empty() { return }       // peer closed
                        let reply = data.to_vec();
                        drop(data);                          // buffer back to the pool
                        if stream.write_all(reply).await.0.is_err() { return }
                    }
                });
            }
        })
}

recv() lends a buffer from the worker's pool. An empty buffer means the peer closed the connection.

UDP and files · giong::net · giong::fs

use giong::{fs, fs::File, net::UdpSocket};

async fn demo() -> std::io::Result<()> {
    let socket = UdpSocket::bind("127.0.0.1:9000")?;
    let (datagram, peer) = socket.recv_from().await?;      // multishot, cancel safe
    let reply = datagram.to_vec();
    drop(datagram);                                        // buffer back to the pool
    socket.send_to(reply, peer).await.0?;

    let file = File::create("log.txt").await?;
    file.write_all_at(b"hello\n".to_vec(), 0).await.0?;
    file.sync_all().await?;
    let text = fs::read("log.txt").await?;
    assert_eq!(text, b"hello\n");
    fs::rename("log.txt", "log.old").await
}

Receiving borrows a buffer from the pool; whatever you hand to the kernel goes in by value and comes back with the result.

design

Each worker owns everything it touches

A worker is a thread pinned to one core, with its own tasks, run queue, timer wheel, receive buffers and ring. Other threads reach it only through an injector and an eventfd.

run ready tasksTasks queue their reads, writes, accepts and closes. Nothing enters the kernel yet.
io_uring_enterSubmit everything, collect everything that is ready. One system call.
fire timersA timing wheel in user space: arming and cancelling make no system call.

↺ repeat

worker 0
  • tasks (!Send)
  • run queue
  • timer wheel
  • receive buffers
  • its own io_uring
worker 1
  • tasks (!Send)
  • run queue
  • timer wheel
  • receive buffers
  • its own io_uring
worker None per core

eventfd + injector, only when another thread wakes a task

Wakers
A waker does not own its task; it holds a slot number and a handle to the worker. Wakers are Send + Sync although tasks are not, without unsafe.
Writes
Ownership passing: write_all(buf) returns (result, buf). The kernel keeps a pointer to the buffer until it is done, and this API makes that sound in Rust.
Backpressure
A slow reader has its multishot receive paused, so TCP slows the sender. A dry pool reclaims buffers that hold unread data. One stalled connection cannot starve the rest.
Timers
A hierarchical timing wheel, six levels of 64 slots, 1 ms ticks. Nearly every timer is an I/O timeout that is cancelled before it fires, so both are O(1).
Panics
A panic in a task is caught and reported through its JoinHandle. One bad connection does not take down the worker.
Shutdown
With a grace period, requests in progress are answered. Then: cancel everything, wait for the kernel to confirm, free. If confirmation does not come, leak on purpose.
UDP
Multishot recvmsg on the same buffer pool as TCP. Every worker binds its own SO_REUSEPORT socket and recv_from is cancel safe.
Old kernels
Three ring setups, tried in order: DEFER_TASKRUN (6.1+), COOP_TASKRUN (6.0), no flags. The fallbacks are tested on every change by forcing them.

crates

Four crates, one version number

The runtime, and the layers a service needs on top of it. Together they serve HTTPS at 144.8k requests/s on one core, against 81.0k for Axum on the same rustls.

giong

The runtime: TCP, UDP, Unix sockets, files, timers, a cross-thread channel and graceful shutdown.

  • Builder::new().workers(n), one worker per core
  • TcpListener, TcpStream, UdpSocket, UnixListener, UnixStream
  • fs::File with read_at and write_at, fs::read, rename, remove_file
  • time::sleep, timeout, spawn, Handle::spawn_on, sync::mpsc

about 5,400 lines · 115 tests

giong-http

A small, strict HTTP/1.1 server with one async handler.

  • Keep-alive, pipelining, bodies by Content-Length or chunked
  • Limits on head and body size; timeouts for idle, slow and slow-reading clients
  • Answers 400 to any request a proxy in front could read differently
  • Server::serve, serve_unix, serve_tls

about 1,850 lines · 46 tests, among them 500,000 random inputs for the parser

giong-tls

Server-side TLS 1.2 and 1.3 on rustls with ring. No OpenSSL.

  • TlsAcceptor::from_pem(cert, key), or bring your own rustls::ServerConfig
  • recv() is cancel safe
  • A cancelled write poisons the sending side instead of corrupting the record stream
  • ALPN and a handshake timeout

about 390 lines · 8 tests · TLS costs 4% on /json

giong-postgres

A PostgreSQL client and pool. The statements of every task pipeline over a few connections.

  • Client::connect, query, execute, transaction, cancel_token
  • A Pool per worker: shared, pipelined statements; get() for a transaction
  • Every statement prepared once per connection and cached
  • TLS verified by default for remote hosts; SCRAM-SHA-256

about 5,300 lines · 73 tests · 2.6× deadpool's statements per core

verified

What has been checked, and what has not

A runtime that hands buffers to the kernel has to earn trust. This is the evidence so far, and the gaps in it.

Checked

  • 169 tests across the three crates, repeated in release mode on all three io_uring setups without a failure.
  • ThreadSanitizer over 157 tests: no data race.
  • valgrind memcheck over the single-threaded tests: no invalid access, no leak.
  • Load: over 250,000 connections opened and closed, 5,000 at once, the client killed abruptly six times. File descriptors return to the idle count and memory does not grow between runs.
  • Graceful shutdown under load: SIGTERM during 40 slow requests. With a grace period, 400 of 400 were answered; without one, 156 of 200.
  • CI runs formatting, clippy, the documentation build, a build with Rust 1.80, the repeated tests, the fallback setups and ThreadSanitizer.

Not checked, and you should know it

  • giong has not run in production.
  • No real 6.0 kernel. The fallback setups were forced on 6.18. That checks giong's code paths, not the behaviour of the old kernel itself.
  • Neither sanitizer nor valgrind can see the kernel writing into memory, so cancellation safety rests on the design and on tests/cancel.rs.
  • The cross-thread wake-up has not been model-checked with loom.
  • giong-tls has had no independent security audit. The cryptography and the protocol are rustls's; giong's part is connecting rustls to the socket.

limits

When giong is the wrong choice

0.2 is complete enough to write real servers. It is also young, narrow on purpose, and Linux only.

Linux only

Kernel 6.0 or newer. There is no epoll, kqueue or IOCP fallback, and there will not be one.

Not Tokio's ecosystem

Crates that need the Tokio runtime (hyper, sqlx, reqwest) do not run on giong. Crates that only use std::future do.

No HTTP/2

giong-http speaks HTTP/1.1 and receives a body in full before it calls the handler. A body over the limit is refused, not streamed.

TLS is server side only

No TLS client, no kTLS, no certificate reload built in.

Tasks do not migrate

An overloaded core is not helped by an idle one. Spread work yourself with spawn_on or a channel.

No asynchronous DNS

bind and connect resolve names with a blocking call. Also missing: Unix datagram sockets, multicast, reading directories.

Pick something else when

Monoio is the mature choice and runs in production at ByteDance. If you need an HTTP client, HTTP/2 or Tokio-compatible crates today, use it.

Glommio is built for storage engines: Direct I/O and priority scheduling are its strengths.

Tokio is the general-purpose runtime. giong is not trying to replace it for every workload.

roadmap

Where giong is going

Milestones in order, with no dates: a milestone is done when its exit criteria are met. 0.3 is about measuring, not features.

  1. 0.1done

    Foundation

    • Executor, io_uring driver, TCP, UDP, files, timers
    • Thread-per-core runtime, busy-poll mode, cross-thread channel
    • Cancellation safety, backpressure, tests and benchmarks
  2. 0.2current

    A runtime you can serve from

    • Graceful connection draining on shutdown
    • Unix sockets, multishot UDP, more file operations
    • giong-http and giong-tls, HTTPS in giong-http
    • ThreadSanitizer and ring fallbacks in CI; 500,000 random parser inputs
  3. 0.3next

    Prove it

    • Multi-core benchmarks on a physical network, 1 to N workers
    • The suite on real 6.0 and 6.1 kernels
    • loom model check of the cross-thread wake-up; cargo fuzz of the parser
    • Soak test; an independent look at giong-tls

    Done when: published multi-core results with their method, and no open memory- or thread-safety finding

  4. 0.4next

    Foundations for services

    • Asynchronous DNS; connect by name, the RFC 8305 way
    • TLS client: the machine's or compiled-in roots, ALPN, mutual TLS
    • Bounded and oneshot channels; a handle from spawn_on; writev
    • Error codes that say how to fix them; AGENTS.md and llms.txt

    Done when: every item with tests, including cancellation tests

  5. cratesplanned

    What a service needs

    • giong-postgres: SCRAM, TLS, pipelining, a pool per core
    • An HTTP client in giong-http; giong-redis with automatic pipelining
    • giong-web: router, middleware, JSON, compression; streaming bodies
    • giong-mcp: a read-only MCP server for coding agents

    Done when: each one passing against the real service in CI and benchmarked against its Tokio counterpart

  6. 0.5planned

    Runs where io_uring is blocked

    • An epoll driver, chosen when io_uring cannot be set up
    • Docker's default seccomp profile works as it is

    Done when: the whole suite passing on both drivers, and the difference measured

  7. 0.6planned

    HTTP/2 and adapters

    • HTTP/2 in giong-http, over TLS with ALPN
    • Adapters for the futures-io / Tokio I/O traits
    • Decide whether a hyper integration is worth maintaining

    Done when: real browsers served over HTTP/2, and h2spec passing

  8. 0.7planned

    Speed

    • Fixed files, zero-copy send, receive bundles
    • Optional SQPOLL and NAPI busy polling; kTLS

    Done when: a documented, reproducible gain for every option that is merged

  9. 1.0planned

    Stable

    • Every public item reviewed against the agent-first principles
    • cargo-semver-checks in CI; every public item documented
    • Limitations reduced to deliberate non-goals
    • At least one production user reporting back

Deliberate non-goals

  • Production on other operating systems: Linux only, io_uring first. An epoll driver (0.5) covers machines where io_uring is blocked; running on macOS for development may follow.
  • Work stealing: tasks stay on the worker that created them.
  • Every workload: giong covers network services and the clients they need, not everything Tokio is used for.
  • Send futures as a requirement: the API stays usable with Rc and RefCell.
  • Macros that hide control flow, and unbounded defaults.

Source: ROADMAP.md

get started

Add it to a project

Two commands and a handler. The crates are on crates.io.

kernel
Linux 6.0 or newer, 6.1+ recommended. Developed and tested on 6.18.
rust
1.80 or newer.
windows
giong does not build on Windows or macOS. Use WSL2 with a 6.x kernel, a Linux virtual machine, or Docker started with --security-opt seccomp=unconfined.
license
MIT OR Apache-2.0, at your option.

1 · Add the crates

cargo add giong
cargo add giong-http

giong-http is optional. For HTTPS, add it with --features tls.

2 · Write a handler and run it

Paste the HTTP example from What the code looks like into src/main.rs, then:

cargo run --release          # listens on port 8080
curl localhost:8080/health   # ok

Or work on giong itself

git clone https://github.com/giong-run/Giong.git && cd Giong
cargo test --workspace --all-features   # the whole suite, every crate

Examples in the repository

Each one runs from a clone of the repository. examples/

examples/echo.rs

A TCP echo server on every core.

cargo run --release --example echo
nc 127.0.0.1 7000

examples/http.rs

A small HTTP/1.1 server: keep-alive, pipelining, idle timeouts, a header size limit and graceful shutdown on SIGINT and SIGTERM. The same routes the benchmarks use.

cargo run --release --example http
curl localhost:3006/json

examples/udp_echo.rs

A UDP echo server, receiving with multishot recvmsg.

cargo run --release --example udp_echo
echo hello | nc -u -w1 127.0.0.1 7001

examples/cat.rs

Prints files to standard output, reading them through io_uring with positional reads.

cargo run --release --example cat -- Cargo.toml

faq

Questions people ask

Short answers. The README has the long ones.

Is giong ready for production?

Not yet. 0.2 is complete enough to write real servers and is tested hard (see what has been checked), but it has not run in production. 1.0 waits for, among other things, at least one production user reporting back.

Does it run on macOS or Windows?

No. io_uring is a Linux interface and there is no fallback. On Windows use WSL2 with a 6.x kernel (uname -r; wsl --update if it is older); on macOS a Linux virtual machine. In Docker, start the container with --security-opt seccomp=unconfined: the default profile blocks io_uring.

Which Linux kernel do I need?

6.0 or newer, 6.1 or newer recommended. giong tries three ring setups in order: DEFER_TASKRUN (6.1+), COOP_TASKRUN (6.0), then no flags.

Can I use hyper, reqwest, sqlx or other Tokio crates?

Not crates that need the Tokio runtime. Crates that only use std::future work. Adapters for the futures-io and Tokio I/O traits are planned for 0.6.

Is it faster than Tokio?

Sometimes, and the page says where. Small responses at 50 connections are a tie. giong makes about 0.03 system calls per request against 2.05, spends the least CPU per request at 5,000 connections, and reads files 5.5 times faster than Tokio on one core. On 14 cores it had the most requests per second at 1, 2 and 8 workers and was level with or behind tuned Monoio at 4 and 6. Every number is over loopback on one machine.

How do I report a security problem?

Privately, through GitHub's vulnerability reporting, not in a public issue.

Why the name?

After Thánh Gióng, the child of Vietnamese legend who lay silent for three years and then rose a giant: next to nothing while idle, every core the moment load arrives.