nexus-async-rt
Single-threaded async runtime for latency-sensitive systems. Built on mio.
Not a tokio replacement — a purpose-built alternative for single-threaded event loops where predictable latency matters more than multi-threaded throughput.
When to use this vs tokio
Use nexus-async-rt when:
- Single-threaded event loop (one core, no work-stealing)
- Predictable tail latency (no scheduler jitter)
- Zero-alloc task spawning on the hot path (slab allocation)
- Futures are
!Send— and that's fine - You're building on the nexus ecosystem (nexus-net, nexus-rt)
Use tokio when:
- Multi-threaded execution or work-stealing
- tokio ecosystem (tower, hyper, tonic, etc.)
- Futures need to be
Sendacross threads - Broad community support matters
Both can coexist in the same process. Use nexus-async-rt for the latency-critical event loop and tokio for everything else.
Quick start
use *;
use WorldBuilder;
let mut world = new.build;
let mut rt = new;
rt.block_on;
What you get
Task spawning
Two strategies, same API:
// Box-allocated — default, no setup needed
let handle = spawn_boxed;
// Slab-allocated — pre-allocated, zero-alloc hot path
let handle = spawn_slab;
Both return JoinHandle<T> — await for the result, drop to detach,
or call abort() to cancel (consumes the handle).
Slab allocation (zero-alloc spawn)
For hot-path tasks where allocation jitter is unacceptable:
// SAFETY: single-threaded runtime owns the slab.
let slab = unsafe ;
let mut rt = builder
.slab_unbounded
.build;
rt.block_on;
Or claim a slot first, spawn later:
if let Some = try_claim_slab
Timers
use Duration;
// Sleep
sleep.await;
// Timeout
let result = timeout.await;
// Interval
let mut tick = interval;
loop
I/O (mio-based)
use ;
// Client
let stream = connect?;
// Server
let listener = bind?;
let = listener.accept.await?;
Channels
Three flavors for different use cases:
use channel;
// Local MPSC — !Send, zero atomics, single-threaded
let = channel;
// Cross-thread MPSC — Sender: Clone + Send
let = channel;
// Cross-thread SPSC — fastest cross-thread path
let = channel;
World access
Access nexus-rt World resources from async tasks:
with_world;
Graceful shutdown
rt.block_on;
Cancellation
let token = new;
let child = token.child_token;
spawn_boxed;
token.cancel; // cancels all children
JoinHandle
spawn_boxed and spawn_slab return JoinHandle<T>:
- Await — get the result:
let val = handle.await; - Detach — drop the handle, task continues, output dropped on completion
- Abort —
handle.abort()consumes the handle, future dropped on next poll - Check —
handle.is_finished()for non-blocking status
JoinHandle is !Send and !Sync — stays on the executor thread.
Performance
| Path | p50 |
|---|---|
| Task dispatch (poll cycle) | 55-64 cycles |
| Local channel try_send+try_recv | 13 ns |
| MPSC channel try_send+try_recv | 22 ns |
| SPSC channel try_send+try_recv | 15 ns |
| Cross-thread channel (busy spin) | 15 ns |
| Cross-thread channel (park/epoll) | 1.7 us |
| Tokio-compat waker bridge | 76 ns |
Features
| Feature | Default | Description |
|---|---|---|
tokio-compat |
No | Adapters for bridging tokio and nexus-async-rt in the same process |
Dependencies
- mio — I/O event loop (epoll/kqueue)
- nexus-rt — World/WorldBuilder for typed resource storage
- nexus-slab — Optional pre-allocated task storage
- nexus-timer — Hierarchical timer wheel
- nexus-queue / nexus-logbuf — Lock-free internal queues
Design Notes
Runtime::block_on is the only entry point for driving the executor. The drain() method was removed -- all task completion is handled within block_on's poll loop.
Cross-thread wakes use a deferred-free strategy: tasks woken from another thread are queued via an intrusive Vyukov MPSC queue and processed on the next executor poll. Task memory is freed on the executor thread, not the waking thread, to avoid cross-thread deallocation.
Platform support
Unix only (#![cfg(unix)]). Linux is the primary target, macOS supported.