Supporting native Rust in Workers with the new Emscripten target for wasm-bindgen¶
First public experimental preview of first-class support for the Emscripten
wasm32-unknown-emscripten Rust compiler target on the
wasm-bindgen toolchain — the goal being to run
native Rust code, including Tokio-based async applications,
directly on Cloudflare Workers rather than
only the wasm32-unknown-unknown target that today's
workers-rs uses. The effort was first initiated by Google over a year ago and
then reviewed/supported by the Cloudflare engineers who co-maintain wasm-bindgen.
Summary¶
Two toolchains that each "assume they are in charge of loading and interacting
with JavaScript" — Emscripten (native-code → Wasm, initially by Mozilla, now
maintained by Google) and wasm-bindgen (Rust↔JS bindings) — were made to
cooperate via a new -sWASM_BINDGEN
setting: Emscripten drives the build, loads the Wasm module, and provides the
companion JS, while wasm-bindgen emits a smaller portable version of its
bindings that slots into Emscripten's library system. Because Workers supports
Web Platform APIs + Node.js compatibility, Emscripten's
Node.js compilation flags let it fully virtualize native platform features
(timers, filesystem, sockets) on top of Workers' existing Node.js APIs. The hard
part was Tokio: Workers are single-threaded, hosted inside a
JS event loop, but Tokio's async is built around blocking-via-thread-parking —
a blocking epoll_wait/socket read cannot block the shared JS event loop.
Cloudflare implemented both reconciliation approaches (JSPI and a new
LocalEventLoop Tokio runtime), contributed the socket/epoll bridge to
Emscripten as -sNODERAWSOCKETS, and demonstrated the whole stack by running a
Rust-native Minecraft server (Pumpkin) inside a
Durable Object with real TCP sockets and
TCP ingress.
Key takeaways¶
-
New Rust target
wasm32-unknown-emscriptenin wasm-bindgen. Two directions now interoperate under-sWASM_BINDGEN: (a) C++ Emscripten builds can link static wasm-bindgen Rust code (both binding layers coexist); (b) Rust apps using wasm-bindgen can build for the Emscripten target (both binding layers coexist). Required changes had to land in both projects and both teams had to agree to maintain integration tests depending on the other. (Source: wasm-bindgen Emscripten docs) -
Emscripten already supports
target_family = unixin Rust, so many libraries — even low-level systems ones — worked out of the box. The few that needed patches (libc,socket2,Mio) mostly just neededtarget_os = "emscripten"added to existing platform gates in place of Wasm gates. Patches were infrequent, mostly trivial, and Rust maintainers were receptive. (Source: patterns/upstream-the-fix) -
Tokio is the hard dependency. Workers are single-threaded inside a JS event loop; Tokio async is designed around blocking supported via threaded parking semantics. "The two models are clearly not compatible with each other — a blocking operation such as a pending socket read or epoll wait cannot block the shared JS event loop." Two options were pursued: WebAssembly JavaScript Promise Integration (JSPI) or modifying Tokio to support event-loop runtime integration. Cloudflare contributed full Tokio support patchsets upstream; the first
wasm32-unknown-emscriptentarget-support patch already landed upstream in Tokio. -
JSPI maps onto Tokio's parking semantics — a blocking Wasm call suspends the Wasm stack on a synchronous op and returns control to the JS event loop, exactly like a park. The subtlety: JSPI can enter a new Wasm stack while an earlier one is suspended, but Rust doesn't know its stack was switched. Tokio's runtime context is a thread-local, and a JSPI stack switch is not a thread switch, so suspended + new stacks share the same thread-local runtime context → the runtime thinks it's still entered and panics. 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; upstreaming ongoing.
-
LocalEventLoop: split Tokio's loop so a host event loop drives it. A Tokio runtime does two things in a loop: (1) poll tasks until nothing is ready, then (2) wait — the thread parks in the I/O driver until a socket is readable / a timer fires / another thread wakes it. When the host already owns an event loop, Tokio's loop can't run inside it without blocking the host's. The fix: split the loop — (1) becomes an explicitdrive()that runs one batch of ready tasks and returns; (2) is replaced by a wake — instead of parking, the runtime tells the host it has work and the host callsdrive()when ready. Built with a standardstd::task::Wakerthe host owns: instead of signaling "poll a future soon," it signals "the runtime itself should be driven soon." Everything that would unpark a native runtime's thread (a spawn, a cross-thread wake, a socket becoming readable, a timer expiring) wakes the host instead. Because aWakerisSend + Syncand 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 one thing you cannot do is wait:block_onstill runs a future as far as ready work carries it, but where a normal runtime would park,LocalEventLoop::block_onpanics (nothing inside the call could wake the future — its wait belongs to the host). Same architecture embeds into a GTK main loop, a Win32 message pump, or a Cocoa run loop, and any number of these runtimes can co-exist due to cooperative execution semantics. API:Builder::build_local_event_loop(..., host_waker)+el.spawn_local(...)(returns immediately) +el.drive(). -
Sockets/epoll bridge via the Node.js compat layer, contributed as
-sNODERAWSOCKETS. With both runtime approaches working, most of Tokio's test custom bridge, Cloudflare reused the one it already has: thenode:netAPI in its Node.js compatibility layer. Emscripten already had-sNODERAWFSto bridge into Node.js FS APIs, so the same approach gave a sockets bridge with no custom APIs on either side. Cloudflare contributed this in over 40 pull requests to Emscripten, now the-sNODERAWSOCKETSlayer — epoll + TCP + UDP + Unix sockets for Emscripten apps in Node.js, and, because Workers implements the samenode:netAPI, on Workers too. Under JSPI,epoll_wait()simply suspends the stack until readiness; forLocalEventLoop, readiness reaches the Waker from a JS callback via a draftedemscripten_epoll_add_listenerAPI, and the nextdrive()collects events with a zero-timeoutepoll_wait()so Tokio's existing I/O driver runs unchanged. -
Proof of concept: a Rust-native Minecraft server (Pumpkin) inside a Durable Object. Dan Lapid ported Pumpkin — a Tokio-based, multi-core-designed Rust Minecraft server — to run inside a Durable Object over a single weekend. A DO has exactly one thread, so Pumpkin's dedicated threads (world-gen thread pool, game-tick loop, chunk scheduler) became cooperative tasks on the event loop: the tick loop + chunk scheduler became async tasks, each Rayon job became a Tokio task, and world generation runs on the event loop itself (one turn per chunk, interleaved with network I/O + game ticks). Persistence: Pumpkin writes its world through ordinary
std::fs; under Emscripten's-sNODERAWFS, FS calls forward to thenode:fsWorkers compat layer, and Dan's worker-fs-mount library mounts anode:fs-compatible FS whosedurable-object-fsbackend stores files as rows in the DO's SQLite storage, written synchronously and committed with the DO's transaction — "Pumpkin itself has no idea it isn't on disk, and a restarted object boots straight from the same world." Networking: each player's connection arrives through Workers TCP ingress at the Worker'sconnect()handler, forwarded into the DO, wherehandleAsNodeConnection()fromcloudflare:nodedispatches the socket to anet.Serverlistening on that port inside the object (a new TCP counterpart ofhandleAsNodeRequest());-sNODERAWSOCKETSimplementsTcpListeneron top ofnet.Server, so the server accepts players exactly as it would on Linux, and the returned promise tells the object when the last player left so it can save and shut down. -
Availability: experimental patchsets + example applications for web, Node.js, and Workers are available today; the Tokio patchsets are required directly in the Rust Workers Tokio examples pending further upstream integration. Feedback solicited via GitHub +
#rust-on-workerson Cloudflare's Discord.
Systems / concepts / patterns extracted¶
- Systems: Emscripten (target +
-sWASM_BINDGEN/-sNODERAWSOCKETS/-sNODERAWFS/ Node.js flags), wasm-bindgen (wasm32-unknown-emscriptentarget), Tokio (JSPI reentrancy +LocalEventLoop), WebAssembly (JSPI), Cloudflare Workers (native-Rust host +connect()TCP ingress +cloudflare:node), Durable Objects (single-thread host for Pumpkin; SQLite-backed FS), Node.js (node:net/node:fscompat as the bridge substrate), workers-rs (Emscripten examples), Mio (Tokio's epoll-based I/O driver — recorded as prose). Pumpkin, Rayon,worker-fs-mount,durable-object-fs, GTK / Win32 / Cocoa run loops recorded as prose. - Concepts: backpressure-adjacent runtime wake/park semantics. Recorded as prose + tags (single-source, not minted as pages per the taxonomy gate): event-loop-runtime-integration, host-driven-runtime-drive, cooperative-time-multiplexed-threading, thread-local-context-swap-on-stack-switch, wake-instead-of-park, node-compat-layer-as-syscall-bridge, single-threaded-host.
- Patterns: patterns/upstream-contribution-parallel-to-in-house-integration
(Tokio/libc/socket2/Mio target patches +
-sNODERAWSOCKETSupstream while the Rust Workers examples ship on the PR branches), patterns/upstream-the-fix (40+ Emscripten PRs + trivial target-gate patches to shared Rust crates).
Operational numbers / caveats¶
- 40+ pull requests contributed to Emscripten for the
-sNODERAWSOCKETSlayer. - First
wasm32-unknown-emscriptentarget-support patch already landed upstream in Tokio; full Tokio patchsets still under upstream review. - Pumpkin DO port done over a single weekend (Dan Lapid).
- Experimental / pre-release throughout: JSPI reentrancy design +
LocalEventLoopdesign are being finalized/upstreamed in collaboration with the Tokio + Emscripten teams; the Rust Workers Tokio examples require the patchsets directly. - Not a benchmark post — no throughput/latency numbers; the claim is "significantly improved library compatibility for Rust Workers" demonstrated qualitatively.
Source¶
- Original: https://blog.cloudflare.com/rust-workers-emscripten-target/
- Raw markdown:
raw/cloudflare/2026-09-28-supporting-native-rust-in-workers-with-the-new-emscripten-ta-30c18d3f.md
Related¶
- systems/tokio — the load-bearing dependency; JSPI reentrancy +
LocalEventLoop. - systems/emscripten —
wasm32-unknown-emscriptentarget +-sNODERAWSOCKETS/-sNODERAWFS/-sWASM_BINDGEN. - systems/wasm-bindgen — the Rust↔JS binding toolchain the Emscripten target lands in.
- systems/webassembly — JSPI is the WebAssembly proposal enabling the park-via-suspend model.
- systems/cloudflare-workers — the single-threaded JS-event-loop host;
connect()TCP ingress +cloudflare:node. - systems/cloudflare-durable-objects — single-thread host for the Pumpkin PoC; SQLite-backed
node:fs. - systems/nodejs —
node:net/node:fscompat layer reused as the socket + FS bridge. - systems/workers-rs — Rust Workers SDK; ships the Emscripten examples.
- patterns/upstream-contribution-parallel-to-in-house-integration — Tokio + Emscripten upstreaming parallel to shipping the integration.
- patterns/upstream-the-fix — 40+ Emscripten PRs + trivial Rust-crate target-gate patches.
- companies/cloudflare