Skip to content

SYSTEM Cited by 3 sources

Tokio

Tokio is the dominant async runtime for Rust (tokio.rs, crate tokio). It integrates Rust Futures with an OS event loop (epoll on Linux, kqueue on BSD/ macOS) so poll-driven state machines only get re-polled when something they care about has actually progressed. The Waker passed to each poll call is the handle the Future uses to wake itself back up through the runtime.

Fly.io's [[sources/2025-02-26-flyio-taming-a-voracious-rust-proxy|2025-02 proxy incident]] gives a usefully terse summary of the model:

"A Future is a type that represents the future value of an asynchronous computation… Futures are state machines, and they are lazy: they expose one basic operation, poll, which an executor (like Tokio) calls to advance the state machine. That poll returns whether the Future is still Pending, or Ready with a result… Tokio integrates Futures with an event loop and, when calling poll, passes a Waker. The Waker is an abstract handle that allows the Future to instruct the Tokio runtime to call poll, because something has happened."

This framing is load-bearing for the rest of that source — every Fly.io bug discussed there is a Waker-level mis-signal that collapses the whole abstraction to busy-polling.

Running Tokio inside a foreign single-threaded event loop (Cloudflare, 2026-09-28)

Tokio's park-in-the-I/O-driver model is exactly what makes it a runtime rather than a library — but it is also what makes it incompatible with a host that already owns an event loop. Cloudflare Workers are single-threaded and hosted inside a JS event loop; "a blocking operation such as a pending socket read or epoll wait cannot block the shared JS event loop." Getting Tokio to run on the new wasm32-unknown-emscripten target (the first target-support patch already landed upstream in Tokio) required reconciling the threaded-parking model with a single-threaded host, and Cloudflare implemented both viable approaches (Source: sources/2026-09-28-cloudflare-supporting-native-rust-in-workers-with-the-new-emscripten-target):

Approach 1 — JSPI maps onto parking, but breaks the thread-local context

WebAssembly JavaScript Promise Integration (JSPI) "maps directly onto Tokio's existing parking semantics": a blocking Wasm call suspends the Wasm stack on a synchronous op and returns control to the JS event loop — exactly a park. The subtlety is reentrancy: JSPI can enter a new Wasm stack while an earlier one is suspended, but Rust doesn't know its stack was switched out from under it. Tokio's runtime context is tracked via a thread-local, and a JSPI stack switch is not a thread switch, so the suspended and new stacks share the same thread-local runtime context → the runtime "still thinks it is in the parked context when a new Wasm call enters, and then panics because the runtime is already entered." Fully reentrant JSPI therefore requires swapping the thread-local context on each JSPI enter, exit, suspend, and resume — "in effect this is cooperative time-multiplexed threading, with each suspended stack carrying its own runtime context." Implemented + verified in the pre-release patchset; design finalization + upstreaming ongoing with the Tokio + Emscripten teams.

Approach 2 — LocalEventLoop: split the loop, replace wait with wake

The more general fix — designed to also work for native GTK / Win32 / Cocoa hosts, not just Wasm-in-JS — splits Tokio's loop in two:

  • (1) poll ready tasks becomes an explicit drive() that runs one batch of ready tasks and returns.
  • (2) wait is replaced by a wake: instead of parking in the I/O driver, the runtime tells the host it has work, and the host calls drive() when it is ready.

LocalEventLoop is a LocalRuntime "whose wait has been replaced by a wake." It is built with a standard std::task::Waker the host owns — but instead of signaling "poll a future soon," the waker signals "the runtime itself should be driven soon." Everything that would unpark a native runtime's thread — a spawn, a task woken from another thread, a socket becoming readable, a timer expiring — wakes the host instead. Because a Waker is Send + Sync and carries no execution semantics, "the contract is safe by construction: a wake arriving from another thread, from a host callback, or even during a drive, queues work rather than re-entering the runtime. The drive that follows runs on the owning thread with nothing else on the stack."

The one thing you cannot do is wait: block_on still runs a future as far as ready work carries it, but where a normal runtime would park, LocalEventLoop::block_on panics — nothing inside the call could wake that future, because its wait belongs to the host. API shape:

let el = Builder::new_current_thread()
    .enable_all()
    .build_local_event_loop(Default::default(), host_waker)?;

el.spawn_local(async {              // returns immediately
    let mut stream = TcpStream::connect(addr).await?;
    let mut buf = [0u8; 1024];
    // control flow returns to the host while waiting
    let n = stream.read(&mut buf).await?;
    Ok::<_, io::Error>(())
});
// host drives to completion via el.drive() calls; when data is
// unavailable the runtime returns control to the host, and Tokio
// uses host_waker to signal it needs driving once the socket is readable.

"The host event loop is never blocked, and interleaves its own work with Tokio's, one batch at a time … any number of these runtimes can co-exist due to their cooperative execution semantics." This is the canonical wiki instance of host-driven runtime integration (a.k.a. wake-instead-of-park / cooperative time-multiplexed scheduling into a foreign event loop) — recorded here as prose rather than minted as a standalone concept page (single source; taxonomy gate).

Sockets: Tokio's net feature over the Node.js compat layer

Tokio's I/O driver is built on epoll_wait() via Mio, but Emscripten only supported poll() + a WebSocket emulation layer — so the whole net feature (TCP/UDP/Unix) was the last unsupported subsystem. Cloudflare bridged it through the Node.js node:net compat layer, contributed to Emscripten as -sNODERAWSOCKETS (40+ PRs). Under JSPI, epoll_wait() simply suspends the stack until readiness so Tokio's I/O driver works as on native; under LocalEventLoop, readiness reaches the Waker from a JS callback via a drafted emscripten_epoll_add_listener, and the next drive() collects events with a zero-timeout epoll_wait() so Tokio's existing I/O driver runs unchanged. Low-level crates libc, socket2, and Mio needed only trivial target_os = "emscripten" gate additions. Cloudflare ran this as upstream contribution parallel to in-house integration — the Rust Workers Tokio examples ship on the patch branches while the full Tokio patchsets are under upstream review.

Seen in

  • sources/2026-09-28-cloudflare-supporting-native-rust-in-workers-with-the-new-emscripten-target — canonical wiki instance of running Tokio inside a foreign single-threaded event loop. JSPI thread-local-context swap for reentrancy + the LocalEventLoop wake-instead-of-park runtime + the node:net/-sNODERAWSOCKETS socket bridge for Tokio's net feature. First wasm32-unknown-emscripten Tokio target patch landed upstream; full Tokio support patchsets under review. Demonstrated by porting the Tokio-based Pumpkin Minecraft server into a Durable Object — its dedicated OS threads (world-gen pool, tick loop, chunk scheduler) became cooperative tasks on the one DO thread.

  • sources/2025-02-26-flyio-taming-a-voracious-rust-proxy — contextual primer. Tokio itself is not the bug; it is the execution substrate that makes Future / Waker / AsyncRead semantics load-bearing. When a TlsStream mis-handles its Waker, Tokio faithfully drives the busy- poll cycle the mis-handling demands — which is exactly what shows up as 100% CPU in the flamegraph.

  • systems/rustls / systems/tokio-rustls — the TLS path that plugs into Tokio via the async adapter.
  • systems/fly-proxy — built on Tokio.
  • systems/emscripten — the wasm32-unknown-emscripten target Tokio gained support for; hosts the -sNODERAWSOCKETS socket bridge.
  • systems/webassembly — JSPI is the WebAssembly proposal that lets a blocking Wasm call suspend the stack like a park.
  • systems/cloudflare-workers — the single-threaded JS-event-loop host.
  • systems/cloudflare-durable-objects — single-thread host for the Pumpkin PoC.
  • patterns/upstream-contribution-parallel-to-in-house-integration — the Tokio + Emscripten upstreaming shape.
  • async-rust-future — the state-machine primitive Tokio drives.
  • rust-waker — the wake-up handle Tokio passes into poll.
  • asyncread-contract — the streaming-IO extension most network code uses.
  • spurious-wakeup-busy-loop — the pathology that appears when a Future lies to Tokio about its readiness.
  • companies/flyio
  • sources/2025-05-28-flyio-parking-lot-ffffffffffffffff — Tokio explicitly ruled out as a source of the parking_lot-RwLock bug: "parking_lot locks are synchronous, but we're a Tokio application; something somewhere could be taking an async lock that's confusing the runtime. Alas, no." Useful negative result — async runtimes and synchronous locks can coexist correctly when used carefully, and the wrong place to look for a synchronous-lock bug is the async runtime. Also reinforces the wider wiki framing that parking_lot locks are synchronous primitives even though Fly uses them from a Tokio application.
Last updated · 766 distilled / 2,225 read