An async runtime for Rust. Every task belongs to a region, and a region doesn't close until everything in it has finished. Cancelling a task asks it to stop instead of dropping it mid-poll. Effects go through an explicit capability context, and a lab runtime replays any schedule from its seed.
Most executors leave task ownership, cancellation, and cleanup to convention. Asupersync makes them part of the runtime: who owns each task, what it still has to finish, and what it's allowed to do. These guarantees cost time per task, and they're enforced by the runtime rather than proven by the type system.
Structured concurrency
No orphan tasks
Every task you spawn through a CxCxThe capability context every async function receivesCx is how a task spawns children, checks for cancellation, reads its budget and the current time, draws randomness, and records traces. It belongs to a region, so anything spawned through it is owned by that region. A test can hand in a lab Cx and the same code runs on virtual time. belongs to that context's regionRegionThe scope that owns tasksEvery task belongs to a region, and regions nest into a tree. Closing a region cancels whatever is still running in it, waits for those tasks to finish, runs finalizers, and resolves obligations before it reports done. Tasks spawned through a Cx belong to that Cx's region; tasks spawned through a RuntimeHandle belong to the root region., and regions nest into a tree. Closing a region cancels whatever is still running in it, waits for those tasks, runs finalizers, and resolves outstanding obligations before it reports done.
There's no detached spawn. Tasks started from a runtime handle belong to the root region and are drained at shutdown. Click a region in the demo to watch a close cascade down the tree.
Cancellation
Cancellation is a request
Dropping a future stops it at its last await, whatever it was in the middle of. Here, cancelling a task asks it to stop. It keeps running until it reaches a cancellation pointCancellation PointWhere a task notices it has been cancelledcx.checkpoint() returns an error once cancellation has been requested, and cancel-aware awaits such as channel receives and lock acquisitions return early the same way. A task that never reaches one is never forcibly stopped, and it holds up its region's close. Loops that do real work should call cx.checkpoint()., drains its own work (it can still await, and it can still return a value), and then its finalizers run. The outcome records Cancelled(reason) with the cause chain.
The flip side: a task that never checks in is never forcibly stopped. It holds up its region's close. Cleanup budgetsBudgetDeadline, poll quota, cost quota, and priority for a piece of workA small Copy struct: an optional absolute deadline, a poll quota, an optional abstract cost quota, and a priority from 0 to 255. Scopes and tasks carry one, and nested budgets combine with meet, so the tighter constraint always wins. On the production runtime, cleanup budgets are advisory: a task that runs past its budget is not killed. are advisory on the production runtime, and Runtime::shutdown_timeout bounds how long you wait.
Two-phase effects
Reserve, then commit
If a task is cancelled halfway through sending a message, the message shouldn't be lost or half-sent. Channel sends are split in two: tx.reserve(&cx).await? claims capacity and commits nothing, and permit.send(v) publishes. Cancelled while reserving? Nothing happened. Dropped the permit? The slot is released.
The demo uses a bank transfer as the analogy. In the library, the pattern covers channels, network sends, and obligation tokens. It isn't a transaction system: partial I/O like write_all documents its own weaker contract.
Obligations
Permits the runtime can count
Reserving a channel slot, taking a semaphore permit, or holding a lease records an obligationObligation SystemThe runtime's table of permits, acks, and leasesWhen a task reserves a channel slot, takes a semaphore permit, or holds a lease through a runtime-built Cx, the runtime records an obligation. Sending, releasing, or aborting resolves it. A region can't close with unresolved registered obligations, and the obligation_leak oracle names any that escape, by kind and holder.. Sending, releasing, or aborting resolves it. A region can't close cleanly while one is outstanding.
Rust can't enforce “use exactly once” at compile time (its types are affine, not linear), so this is runtime bookkeeping: a permit that escapes through mem::forget is reported by the lab's leak oracle, by kind and holder.
Capabilities
Effects go through Cx
Async functions take &Cx. Spawning, reading time, randomness, tracing, and runtime-managed I/O go through it, so a function's signature says what it can do, and handing it a lab Cx runs the same code on virtual time.
A context can be narrowed (Cx::restrict) but not widened again, and plain I/O entry points refuse when the calling task lacks the IO capability. Some host-boundary code, like OS entropy for temp-file names, is documented as outside the boundary.
Deterministic testing
The lab runtimeLab RuntimeThe deterministic test runtimeRuns async code on virtual time with a seeded scheduler, so the same seed reproduces the same schedule. It captures traces, injects cancellation and chaos deterministically, detects futurelocks, writes crashpacks for failing runs, and checks oracles. Concurrency bugs become reproducible test failures instead of flakes.
Run your async code on virtual time with a seeded scheduler and the same seedSeedThe number that fixes a lab scheduleThe lab scheduler's choices come from a seeded deterministic RNG, so the same seed reproduces the same interleaving, timer order, and chaos injections. A failing seed is a reproducible bug report. reproduces the same interleaving. Sleeps complete instantly, traces are captured, and oracles check every run for leaked tasks, leaked obligations, undrained race losers, and protocol violations.
A schedule explorer derives new seeds from the races it detects and skips schedules equivalent to ones it has seen. It's a bug finder, not a proof of exhaustive coverage, but a failing seed is a reproducible test instead of a flake.
Spork
OTP-style actors that can't be orphaned
SporkSporkAsupersync's OTP-style layerSupervision, a name registry, and actors on top of regions, obligations, and explicit cancellation: GenServers, supervisors, links, monitors, process groups, and AppSpec for declarative topologies. Processes always belong to a region and can't be detached, and restart and DOWN-message order is deterministic under the lab runtime. brings Erlang's vocabulary to regions: GenServersGenServerAn OTP-style stateful serverA task that owns state and handles call, cast, and info messages from a bounded mailbox, spawned with cx.spawn_gen_server. Each call hands the server a Reply that wraps a tracked obligation: the server must send or abort it. Dropping it unanswered panics outside of cancellation and unwinding, and the lab's reply_linearity oracle checks the same rule. This is enforced at runtime, not by the compiler., actors with bounded mailboxes, monitors, links, and supervisorsSupervisorRestarts failed children by policyA Spork supervisor compiles a restart topology over regions: boot order, dependencies, and shutdown budgets. Run live with CompiledSupervisor::bind_managed, it cancels and drains a failed child, then restarts it one-for-one, one-for-all, or rest-for-one, within shared intensity and backoff limits. that restart children one-for-one, one-for-all, or rest-for-one. Every process belongs to a region.
Each call hands the server a reply obligation. Dropping it unanswered fails loudly instead of leaving the caller waiting, and under the lab runtime restarts and DOWN messages arrive in a fixed order.
RaptorQ
Fountain codes, and a transfer protocol on top
The runtime includes an RFC 6330 RaptorQRaptorQThe RFC 6330 fountain codeA systematic fountain code: the first symbols are the source data itself, and repair symbols can be generated without limit. With K source symbols, receiving K usually suffices and K + 2 almost always does. Asupersync's implementation is deterministic, with a policy-driven decode planner and optional SIMD GF(256) kernels. codec: the sender produces as many encoded symbols as it likes, and any sufficient set rebuilds the data, so packet loss costs bandwidth instead of round trips.
Its main user is ATP, a file-transfer protocol with Merkle-verified commits, resumable journals, and multi-donor pulls. See the ATP page for measurements, including where it loses to rsync.
More in the demos
The testing machinery, the scheduler internals, and the math, each with an interactive visualization and a plain account of what's on by default.
The APIs are deliberately different: Asupersync trades implicit convenience for explicit cancellation. The concepts still line up.
tokio primitives and their Asupersync equivalents
tokio
Asupersync
What changes
tokio::spawn(fut)
cx.spawn(|cx| async move { … })
The task belongs to the caller's region, and the closure gets its own Cx.
JoinHandle<T>
TaskHandle<T>
.join(cx).await keeps cancellation and panic as separate outcomes.
JoinSet<T>
JoinSet::in_cx(cx)
join_next, join_all, and cancel_all all keep ownership of the drain.
tokio::select!
race!(cx, { … })
Returns only after every loser, and anything a loser spawned, has been cancelled and drained.
FuturesUnordered
cx::fiber::scope(|s| …)
Borrowing futures, no 'static bound, all on the calling task's thread.
time::sleep(dur)
sleep(cx.now(), dur)
Takes the current time explicitly, so the lab can run it on virtual time.
time::timeout(dur, fut)
scope.timeout(&cx, dur, |cx| op)
On expiry the operation is cancelled and drained, and a late result is reported.
mpsc::channel(n)
channel::mpsc::channel(n)
tx.reserve(&cx).await?.send(v). A one-call send() exists for the common case.
sync::Mutex
sync::Mutex
lock(&cx).await? can be cancelled while waiting, so it returns a Result.
sync::Semaphore
sync::Semaphore
acquire(&cx, n).await?. Each permit is an obligation the runtime tracks.
Surprise 1
Every async operation takes &Cx
tokio reads runtime state from thread-locals. Asupersync passes a capability context, so cancellation and budgets flow through the call graph and a function's signature shows what it can do.
Surprise 2
No fire-and-forget
tokio::spawn detaches. Here every task lives in a region, and leaving a scope waits for its children. A task that never reaches a cancellation point holds up that close, which is the point: you find out.
Surprise 3
Outcome has four variants
Ok, Err, Cancelled(reason), and Panicked(payload), ordered by severity. Combinators aggregate with that order, so a cancellation or panic can't be masked by a sibling's success.
{12 break; // cancelled, or the receiver closed13 };14
let
Outcome::Ok(()) = permit.send(i)
else
{15 break; // the receiver left after we reserved16 };17sleep(cx.now(), Duration::from_millis(100)).
await
;18 }19 }).expect("spawn producer");2021 while
let
Ok(val) = rx.recv(&cx).
await
{22println!("got: {val}");23 }2425
let
_ = producer.join(cx).
await
;26}
Syntax_Validation_Active
UTF-8_ENCODED
The same producer and consumer. The Asupersync version is longer for three reasons: the send reserves capacity first so a cancelled producer never loses a message, &cx is threaded through every wait, and the producer is owned by the caller's region and joined instead of detached.
Measured against tokio
What it costs
Every task carries a region membership, a cancellation state machine, and a terminal-result channel, and permits are tracked as obligations. That bookkeeping isn't free.
Per-operation cost, Asupersync vs tokio
Workload
Asupersync
tokio
Ratio
spawn + join from a task, 4 workers
4.2–12.4 µs
0.31–0.68 µs
9–27×
spawn + join from block_on, current-thread
3.0–7.1 µs
0.21–0.51 µs
10–14×
yield_now, 4 workers
0.35–0.58 µs
0.23–0.56 µs
1.0–2.0×
yield_now, current-thread
0.30–0.57 µs
0.09–0.15 µs
3.2–3.9×
mpsc ping-pong round trip, 4 workers
1.26–1.77 µs
0.20–0.57 µs
3.1–6.3×
mpsc ping-pong round trip, current-thread
1.26–1.76 µs
0.17–0.30 µs
5.8–7.5×
fan-out child: fiber::scope vs spawn + join, current-thread
0.21–0.56 µs
0.21–0.51 µs
0.9–1.1×
fan-out child: fiber::scope vs spawn + join, 4 workers
Spawning a task, a current-thread yield, and a channel round trip are still several times slower than tokio. A yield on four workers is within 2×. Loopback TCP is 1.1–1.7× slower, and the HTTP/1.1 server handles about half of hyper's requests per second.
For most servers, a few microseconds per task disappear next to network and disk latency. If you spawn millions of tiny tasks per second or exchange messages in a tight loop, the difference matters. For fan-out inside one task, fiber::scope costs about what a tokio task does.
How these were measured: both runtimes in one process, release build with default features, p50 per operation at n = 1,000, on 2026-10-05. Ranges span three hosts (16, 10, and 64 CPUs), two of them shared with other builds, so host noise is part of the range. The HTTP rows come from one 64-CPU host and compare against hyper 1.x.
Reproduce with benches/runtime_vs_tokio.rs. The numbers already include this month's cuts: an O(1) region task set, an obligation-free one-call mpsc::send, a LIFO wake slot, and no global lock per dispatch.
In the box
What ships today
Asupersync brings its own stack instead of wrapping tokio's. Each card says where that piece stands, because support varies by surface.
Runtime and scheduler
A multi-thread work-stealing scheduler with three lanes (cancel, timed, ready), a current-thread flavor, and an on-demand blocking pool. Linux epoll is the main reactor, with optional io_uring; the BSD and Windows reactors accept fewer interest flags.
Built in
Channels and sync
mpsc, oneshot, broadcast, watch, and session channels with reserve-then-send. Mutex, RwLock, Semaphore, Barrier, Notify, OnceCell, and an object pool. Every wait takes &Cx and can be cancelled.
Built in
Combinators
join, race, and timeout drain their losers instead of dropping them. Beyond those: quorum, hedge, first_ok, pipeline, map_reduce, circuit_breaker, bulkhead, rate_limit, bracket, and retry.
Built in
Fibers
cx::fiber::scope runs futures that borrow from the task's stack concurrently, each with its own cancellation. A fiber costs about what a tokio task does. Fibers share one thread, so reach for tasks when you want parallelism.
Built in
Networking
TCP, UDP, and Unix sockets, DNS, TLS 1.2/1.3 through rustls, and WebSocket with an RFC 6455 conformance suite. Under the lab runtime, a virtual TCP stack replaces kernel sockets.
Built in · TLS behind a feature
HTTP and gRPC
HTTP/1.1 and HTTP/2 servers, a pooled HTTP/1.1 client, gRPC, and a small router with extractors, middleware, and SSE. It isn't axum: handlers use Asupersync's own Request and Response types.
Built in
QUIC and HTTP/3
Native QUIC and HTTP/3 with no tokio underneath, including a multi-peer listener over real UDP with streaming request bodies. Multi-connection deployment, 0-RTT, migration, and interop with other stacks are still unproven.
Feature-gated · partial
Databases
PostgreSQL (binary protocol, SCRAM-SHA-256) and MySQL clients that speak the wire protocol directly over TcpStream, plus SQLite on the blocking pool. Prepared statements, transactions, and connection reuse.
Feature-gated
Actors and supervision
Spork, the OTP-style layer: GenServers, actors, monitors, and links that run as tasks in the caller's region. Supervision trees restart failed children one-for-one, one-for-all, or rest-for-one with intensity and backoff limits.
Built in
Remote tasks
Region-owned remote spawn over a native TCP + mutual-TLS protocol, with leases that count as obligations, an idempotency store for retries, and saga compensation. Route persistence and WAN reliability are open work.
Built in · scoped
RaptorQ and ATP
An RFC 6330 fountain codec with a deterministic decode planner. Its main consumer is ATP, the file-transfer protocol: QUIC or TCP, Merkle-verified commits, resumable journals, and multi-donor bonded pulls.
Built in
Browser Edition
A wasm32 build with JS/TS packages for the main thread and dedicated workers. In the browser it's a ledger of regions, scopes, and task handles over the host's promises, with fetch, WebSocket, and WebTransport behind capabilities.
Release candidate
Observability
Structured logs carrying task and region IDs, counters, gauges, and histograms with an optional OpenTelemetry exporter, a live task inspector, and diagnostics that explain why a task is blocked or why it was cancelled.
Built in
Lab runtime
Virtual time, seeded scheduling, trace capture and replay, deterministic chaos injection, futurelock detection, crashpacks, and race-guided schedule exploration that skips runs equivalent to ones it has already seen.
Built in
How it compares
Runtime comparison
Where Asupersync is stronger, where it's weaker, and where the honest answer is a caveat.
Feature
Asupersync
Tokio
async-std
smol
Structured concurrency
✓Every task belongs to a region
⚠Opt-in (JoinSet, TaskTracker)
✕Manual
✕Manual
Cancellation
⚠Request → drain → finalize, cooperative
⚠Drop, or CancellationToken
⚠Drop
⚠Drop
Orphan tasks
✓None from Cx spawns
✕spawn detaches
✕spawn detaches
✕spawn detaches
Bounded cleanup
⚠Advisory budgets
✕Best-effort
✕Best-effort
✕Best-effort
Deterministic testing
✓Built-in lab runtime
⚠Paused time; loom, turmoil
✕External tools
✕External tools
Obligation tracking
✓Permits tracked, leaks reported
✕None
✕None
✕None
Per-task cost
⚠Several times tokio's
✓The reference point
—
—
Ecosystem
⚠Broad built-in stack; tokio crates need adapters
✓Largest; most crates assume it
⚠Medium
⚠Small
Maturity
⚠Pre-1.0, experimental
✓Production
Discontinued in 2025
✓Production
The code
A scope that cleans up after itself
A budgeted scope, a JoinSet that owns its fan-out, and a join that can't return while any child is still running.
src/main.rs
01
use
asupersync::{main, prelude::*};0203#[main]04
async
fn
main(cx: &Cx) {05// A scope whose tasks get at most 64 polls each.06