Overview
ioxide is an io_uring runtime for .NET. It runs one reactor per core, each owning its own ring and serving connections with no locks and no thread hops - your handler resumes right where the I/O completed, on the same thread. This page is the gentle introduction; the Internals pages go inside the machine.
The idea in one minute
Classic async servers cross a syscall (and often a thread) for every read and write. io_uring changes the deal: you stage many operations in a shared submission queue, enter the kernel once, and collect a whole batch of completions. ioxide builds a thread-per-core server on top of that - and adds the part that makes it pleasant to write: when an operation completes, the awaiting handler continues inline, on the reactor thread, with nothing scheduled anywhere.
- One reactor per core. Each is a thread with its own ring, listener, buffers, and connection pool. Nothing is shared between them - no locks on the hot path.
- Inline resume.
await conn.ReadAsync()parks your handler; the moment the recv completes, the reactor calls back into your code directly. No thread pool, no synchronization context. - Zero-copy where it counts. Received bytes are parsed straight out of kernel-shared buffers; responses are staged in a per-connection slab and sent in one op.
- Zero native dependencies in the core - io_uring is driven through raw syscalls, no liburing.
The shape of a server
You set a config, spawn one reactor per core on its own thread, and give each a
TcpHandle - the per-connection loop. That loop reads, does whatever work, writes,
and flushes; it can await a database, a cache, or a file, and resume inline.
var config = new ServerConfig { ReactorCount = Environment.ProcessorCount }; // Tcp.Port defaults to 8080
var threads = new Thread[config.ReactorCount];
for (int i = 0; i < config.ReactorCount; i++)
{
var reactor = new Reactor(i, config);
reactor.TcpHandle = async (r, conn) =>
{
try
{
while (true)
{
var snapshot = await conn.ReadAsync(); // io_uring recv, resumes inline
while (conn.TryGetItem(snapshot, out var item)) // drain the received slices
if (item.HasBuffer) conn.ReturnBuffer(in item);
conn.Write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"u8);
await conn.FlushAsync(); // io_uring send
if (snapshot.IsClosed) return;
conn.ResetRead();
}
}
finally { conn.DecRef(); }
};
threads[i] = new Thread(reactor.Run) { IsBackground = false };
threads[i].Start();
}
foreach (var t in threads) t.Join();
ioxide does not speak HTTP for you - you write the response bytes. That is deliberate: the runtime is the I/O engine, and what rides on it is yours. The landing page shows the same loop with real request parsing and a database call.
Plugging in backends
Anything your handler talks to - Postgres, Redis, files, an upstream service - is a client that rides the same ring, opened once per reactor and rented per request. The engine never names a client type, so adding one never touches the reactor:
ioxide.pg- pooled ring-native Postgres.ioxide.redis- full RESP2 Redis client.ioxide.file- static files with baked responses.ioxide.tls- kernel TLS, plus a portable SslStream path.- Your own - the same seam in under 100 lines.
Where to go next
The documentation has two halves, for two readers. This Guide is the using-it path - a page per client (Postgres, Redis, TLS, static files), plus multi-port listeners and writing your own custom client. Read what you need and stop.
Internals is the understanding-and-contributing path. Start with Architecture for the system view - the submit/wait loop, provided buffer rings, completion routing by generation, connection lifetime - then read any package in full: the core, ioxide.pg, ioxide.redis, ioxide.tls, and ioxide.file, each annotated with code straight from the source.