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
Futureis 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. Thatpollreturns whether the Future is stillPending, orReadywith a result… Tokio integrates Futures with an event loop and, when callingpoll, passes aWaker. The Waker is an abstract handle that allows the Future to instruct the Tokio runtime to callpoll, 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
LocalEventLoopwake-instead-of-park runtime + thenode:net/-sNODERAWSOCKETSsocket bridge for Tokio'snetfeature. Firstwasm32-unknown-emscriptenTokio 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/AsyncReadsemantics load-bearing. When aTlsStreammis-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.
Related¶
- 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-emscriptentarget Tokio gained support for; hosts the-sNODERAWSOCKETSsocket 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
Futurelies 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_lotlocks 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.