v0.5.0 on crates.io · 0.6.0 on main

The
Cancel-Correct
Async Runtime.

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.

RsCxCx
ASUPERSYNC RUNTIMERoot RegionServer RegionWorker RegionClosing Regiontasks (owned)THREE-LANE SCHEDULERCancel LaneTimed Lane (EDF)Ready LaneRequest → Drain → FinalizeCx (capabilities)
Runtime_Architecture
0Orphan tasks
  • Outcome variants
    4

    Ok, Err, Cancelled, Panicked

  • Built-in lab oracles
    24

    9 are fed from runtime state today

  • Lean-checked invariants
    6

    Of the abstract model, not the Rust code

  • tokio in the default build
    0

    No normal-edge dependency

What the runtime keeps track of

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.

Coming from tokio

What maps to what

The APIs are deliberately different: Asupersync trades implicit convenience for explicit cancellation. The concepts still line up.

tokio primitives and their Asupersync equivalents
tokioAsupersyncWhat 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.
FuturesUnorderedcx::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::Mutexsync::Mutexlock(&cx).await? can be cancelled while waiting, so it returns a Result.
sync::Semaphoresync::Semaphoreacquire(&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.

tokio
01
use
tokio::sync::mpsc;
02
use
tokio::time::{sleep, Duration};
03 04#[tokio::main]05
async
fn
main() {
06
let
(tx,
mut
rx) = mpsc::channel(10);
07 08 tokio::spawn(
async
move
{
09
for
i
in
0..5 {
10 tx.send(i).
await
.unwrap();
11 sleep(Duration::from_millis(100)).
await
;
12 }13 });14 15 while
let
Some(val) = rx.recv().
await
{
16 println!("got: {val}");17 }18}
Syntax_Validation_Active
UTF-8_ENCODED
asupersync
01
use
asupersync::channel::mpsc;
02
use
asupersync::{Cx, Outcome};
03
use
asupersync::time::sleep;
04
use
std::time::Duration;
05 06
async
fn
run(cx: &Cx) {
07
let
(tx,
mut
rx) = mpsc::channel::<i32>(10);
08 09
let
mut
producer = cx.spawn(
move
|cx|
async
move
{
10
for
i
in
0..5 {
11
let
Ok(permit) = tx.reserve(&cx).
await
else
{
12 break; // cancelled, or the receiver closed13 };14
let
Outcome::Ok(()) = permit.send(i)
else
{
15 break; // the receiver left after we reserved16 };17 sleep(cx.now(), Duration::from_millis(100)).
await
;
18 }19 }).expect("spawn producer");20 21 while
let
Ok(val) = rx.recv(&cx).
await
{
22 println!("got: {val}");23 }24 25
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
WorkloadAsupersynctokioRatio
spawn + join from a task, 4 workers4.2–12.4 µs0.31–0.68 µs9–27×
spawn + join from block_on, current-thread3.0–7.1 µs0.21–0.51 µs10–14×
yield_now, 4 workers0.35–0.58 µs0.23–0.56 µs1.0–2.0×
yield_now, current-thread0.30–0.57 µs0.09–0.15 µs3.2–3.9×
mpsc ping-pong round trip, 4 workers1.26–1.77 µs0.20–0.57 µs3.1–6.3×
mpsc ping-pong round trip, current-thread1.26–1.76 µs0.17–0.30 µs5.8–7.5×
fan-out child: fiber::scope vs spawn + join, current-thread0.21–0.56 µs0.21–0.51 µs0.9–1.1×
fan-out child: fiber::scope vs spawn + join, 4 workers0.20–0.39 µs0.31–0.68 µs0.55–0.85×
Server-shaped throughput, Asupersync vs tokio
WorkloadAsupersynctokiotokio faster by
TCP request/response, 1 connection30–44K round trips/s46–76K1.55–1.7×
TCP request/response, 64 connections93–129K round trips/s123–144K1.1–1.3×
HTTP/1.1 keep-alive GET, 1 connection (vs hyper)22K requests/s44K2.0×
HTTP/1.1 keep-alive GET, 64 connections (vs hyper)64K requests/s134K2.1×

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
Tokioasync-stdsmol
Structured concurrency
Every task belongs to a regionOpt-in (JoinSet, TaskTracker)ManualManual
Cancellation
Request → drain → finalize, cooperativeDrop, or CancellationTokenDropDrop
Orphan tasks
None from Cx spawnsspawn detachesspawn detachesspawn detaches
Bounded cleanup
Advisory budgetsBest-effortBest-effortBest-effort
Deterministic testing
Built-in lab runtimePaused time; loom, turmoilExternal toolsExternal tools
Obligation tracking
Permits tracked, leaks reportedNoneNoneNone
Per-task cost
Several times tokio'sThe reference point——
Ecosystem
Broad built-in stack; tokio crates need adaptersLargest; most crates assume itMediumSmall
Maturity
Pre-1.0, experimentalProductionDiscontinued in 2025Production
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::*};
02 03#[main]04
async
fn
main(cx: &Cx) {
05 // A scope whose tasks get at most 64 polls each.06
let
scope = cx.scope_with_budget(Budget::new().with_poll_quota(64));
07
let
mut
tasks = JoinSet::new(&scope);
08 09
for
value
in
1..=2_u32 {
10 tasks11 .spawn(cx,
move
|_|
async
move
{ Ok::<_, Error>(value) })
12 .expect("spawn region-owned task");13 }14 15
let
results = tasks.join_all(cx).
await
;
16 // Nothing spawned into the set is still running here.17 assert_eq!(results.len(), 2);18}
Syntax_Validation_Active
UTF-8_ENCODED
Roadmap

Where things stand

Seven phases, from a single-thread deterministic kernel to ongoing hardening. Partial means partial.

Phase 0 · Complete

Deterministic single-thread kernel

  • Regions, tasks, and the cancellation state machine

  • A four-valued Outcome ordered by severity: Ok < Err < Cancelled < Panicked

  • The lab runtime: virtual time, seeded scheduling, trace capture

Runtime Log v0.2
Phase 1 · Complete

Parallel scheduler and region heap

  • Three-lane work-stealing scheduler: cancel, timed (EDF), and ready

  • Region heap with generation-checked handles, reclaimed when the region closes

  • Optional sharded runtime state with a fixed lock order

Runtime Log v0.2
Phase 2 · Partial

I/O and protocols

  • epoll reactor with optional io_uring; narrower BSD and Windows reactors

  • TCP, HTTP/1.1, HTTP/2, TLS, WebSocket, gRPC, and database clients

  • Native QUIC and HTTP/3; deployment and interop evidence still open

Runtime Log v0.2
Phase 3 · Complete

Actors and supervision

  • GenServers, actors, monitors, and links, on the native runtime and in the lab

  • Live supervision trees: one-for-one, one-for-all, rest-for-one

  • Restart intensity and backoff limits

Runtime Log v0.2
Phase 4 · Core complete

Distributed structured concurrency

  • Region-owned remote spawn over TCP with mutual TLS

  • Leases as obligations, an idempotency store, saga compensation

  • RaptorQ snapshot distribution with quorum recovery

Runtime Log v0.2
Phase 5 · Partial

Schedule exploration and formal tooling

  • Race-guided seed exploration that skips equivalent traces

  • TLA+ export of lab traces, checked by TLC

  • Lean checks six model invariants; there is no Rust refinement proof yet

Runtime Log v0.2
Phase 6 · Ongoing

Hardening

  • Benchmark, golden-output, flamegraph, and proof-note gates before each commit to main

  • Browser Edition packages, currently a release candidate

  • Cutting per-task overhead, measured against tokio in the same process

Runtime Log v0.2

Try it

The first program needs one import and an attribute. Default features build on stable Rust 1.95 or newer.

terminal
$cargo add asupersync
MIT license (with an OpenAI/Anthropic rider) · pre-1.0