Emscripten brings Tokio and native Rust libraries to Cloudflare Workers
On 28 September 2026, Cloudflare announced the first public experimental preview of first-class support for the wasm32-unknown-emscripten Rust compiler target in wasm-bindgen and in Cloudflare's Rust Workers. The announcement matters because it attacks a long-standing boundary in Rust-on-Workers development: many Rust libraries assume native platform facilities, Unix-like target behavior, or an async runtime model that does not map cleanly onto a JavaScript-hosted WebAssembly environment. By combining Emscripten's platform virtualization with wasm-bindgen's Rust-to-JavaScript bindings, Cloudflare says it can run a wider range of native-style Rust code on Workers, including applications built around Tokio.
The post is explicitly framed as pre-release and experimental. That caveat is important, but the technical scope is still notable. Cloudflare describes three concrete example workflows that are available to try: building Emscripten Rust Workers, running the Tokio async runtime in a Worker, and using TCP sockets on Workers with Emscripten and Tokio. As a proof of capability, Cloudflare engineers also report getting Pumpkin, a Rust-native Minecraft server built on Tokio, running inside a Durable Object with TCP ingress. The example is not presented as a production recommendation; it is presented as evidence that the integration can bridge real networking, file system, and async runtime behavior.
What Cloudflare announced
Cloudflare's Rust Workers run on the V8-based Workers Runtime, with wasm-bindgen providing the bridge between Rust-generated WebAssembly and JavaScript. The new work adds support for compiling Rust through the Emscripten target instead of treating Emscripten and wasm-bindgen as mutually exclusive toolchains. According to the Cloudflare post, enabling this target in wasm-bindgen was a long-term effort that began at Google more than a year before the announcement, with Cloudflare engineers later reviewing and supporting the work as maintainers of wasm-bindgen.
The announcement connects three layers: the Rust compiler target (wasm32-unknown-emscripten), the binding/tooling layer (wasm-bindgen), and the runtime platform (Cloudflare Workers and Durable Objects). Emscripten is the key platform adapter. Cloudflare describes Emscripten as an open-source WebAssembly compiler toolchain, initially created by Mozilla and currently maintained by Google engineers, that can virtualize native platform features such as timers, file system operations, sockets, and other native functionality. Because Workers supports Web Platform APIs and Node.js compatibility, Cloudflare states that it can support Emscripten using its Node.js compilation flags and virtualize those native features on top of existing Node.js APIs.
This is a different story from "Rust compiles to Wasm, therefore any Rust code runs anywhere." The practical obstacle has always been the boundary between what Rust code expects and what the host environment provides. A library that wants Unix sockets, blocking I/O semantics, file system calls, or a specific async executor can compile to WebAssembly and still fail at runtime or require invasive rewrites. The Emscripten target does not magically make every dependency portable, but it gives the toolchain a path to emulate or bridge native behavior where the host can provide compatible primitives.
Why wasm-bindgen and Emscripten needed to cooperate
The core technical problem described in the post is that Emscripten and wasm-bindgen historically assumed control over similar responsibilities. Both could generate JavaScript glue, load WebAssembly modules, and define how native or Rust code interacts with JavaScript. Mitch Foley of Google's Portable Toolchains team encountered the issue when an internal Google team wanted to use wasm-bindgen for Rust-JavaScript bindings while still linking C++ dependencies through Emscripten. The goal was for wasm-bindgen to drive the build and produce companion JavaScript, while Emscripten's linker (wasm-ld) could link in the C++ dependencies.
That setup was blocked because the toolchains conflicted over ownership of module loading and JavaScript generation. Cloudflare reports that the practical result was a forced choice: use one toolchain or the other, not both. For Google's Portable Toolchains team, supporting two parallel approaches would have doubled the support surface. Foley and colleague Yifan Yang proposed a cooperative model in which Emscripten would continue to drive the build, load the WebAssembly module, and provide companion JavaScript, while wasm-bindgen would produce a smaller portable version of its bindings in a format Emscripten's library system could include.
The changes required coordination across both projects. Cloudflare says feedback came from Google's Portable Toolchains team, Google's Wasm Tools team, and Cloudflare engineers, and that the necessary changes landed in both Emscripten and wasm-bindgen. The result is exposed through Emscripten's -sWASM_BINDGEN configuration. Under that model, Cloudflare describes two supported directions: C++ Emscripten code compiled by Emscripten can link against static wasm-bindgen Rust code, and Rust applications using wasm-bindgen compiled by Rust's toolchain can target Emscripten while using Emscripten's bindings layer alongside wasm-bindgen's bindings layer.
For developers, the important point is not the internal politics of toolchain ownership but the interoperability contract. The post points readers to the wasm-bindgen Emscripten documentation for details on using the target. The broader implication is that "Rust WebAssembly bindings" and "native C/C++-style WebAssembly platform emulation" no longer have to be treated as separate ecosystems. That matters in real codebases where Rust crates, C dependencies, JavaScript host APIs, and platform expectations meet in the same deployment artifact.
Library compatibility is better, but not automatic
After the target support landed in wasm-bindgen, Cloudflare engineers prototyped Emscripten Rust Workers and reported clear success for many libraries, including some low-level systems libraries. The reason, according to the post, is that Emscripten already supports target_family = unix in Rust. That target-family support can reduce the amount of special-case platform code required for libraries that distinguish broadly between Unix-like and non-Unix environments.
Compatibility was not universal. Cloudflare says some low-level libraries that were unaware of Emscripten required patching, naming libc, socket2, and Mio as examples. The patches were described as mostly trivial: adding the Emscripten target to existing platform gates, such as allowing target_os = "emscripten" where WebAssembly platform gates were too narrow. Cloudflare also reports that these target-support patches were posted fairly infrequently and that Rust library maintainers were receptive to reviewing them.
This detail is useful because it separates two kinds of portability work. One kind is infrastructure: making toolchains cooperate, defining target behavior, and providing host bridges for timers, files, and sockets. The other kind is ecosystem hygiene: teaching individual crates that Emscripten is a valid target with specific capabilities. The second category can be labor-intensive across a large dependency graph, but trivial patches that fit existing platform abstractions are much easier to upstream than wholesale rewrites.
Tokio was the exception. Cloudflare identifies Tokio as a critical foundational library that needed significant support. That makes sense: Tokio is not just another crate with a few platform-specific system calls. It is an async runtime whose execution model, I/O driver, and assumptions about blocking and parking interact directly with the host's scheduling model.
Making Tokio fit a single-threaded JavaScript host
Cloudflare Workers are single-threaded and hosted inside a JavaScript event loop. Tokio async, in the form commonly used on native platforms, is designed around runtime behavior that can include blocking operations handled through threaded parking semantics. The post states the incompatibility directly: a blocking operation such as a pending socket read or epoll wait cannot block the shared JavaScript event loop.
Cloudflare considered two approaches. One was WebAssembly JavaScript Promise Integration (JSPI). The other was to modify Tokio to support event-loop runtime integration. The team reports contributing Tokio patchsets that are under upstream review, with the first target-support patch for wasm32-unknown-emscripten already landed upstream in Tokio. For the examples published alongside the announcement, Cloudflare says the patches are still required directly pending further upstream integration.
JSPI and reentrant runtime context
JSPI can map onto Tokio's existing parking semantics because it allows a blocking WebAssembly call to suspend its stack on a synchronous operation and return control to the JavaScript event loop. That behavior resembles a park: the Wasm side pauses while the host continues to run. The complication appears when a new Wasm call enters while an earlier stack is suspended. JSPI supports multiple suspended WebAssembly stacks, but Rust code may not know that its logical execution context has been switched.
Cloudflare explains the issue through Tokio's runtime context. Tokio tracks runtime context through a thread local, but a JSPI stack switch is not a thread switch. The suspended stack and the newly entered stack share the same thread-local runtime context. As a result, Tokio can believe it is still in a parked context when a new Wasm call enters and panic because the runtime is already entered.
The fix described in the post is careful thread-local handling: swap the thread-local context on each JSPI enter, exit, suspend, and resume. Cloudflare characterizes this as cooperative time-multiplexed threading, where each suspended stack carries its own runtime context. The Cloudflare engineers report implementing and verifying this model in a pre-release Tokio patchset, while noting that finalizing the design and upstreaming it is ongoing in collaboration with the Tokio and Emscripten teams.
This part of the work shows how "just enable the target" is rarely sufficient for runtimes. The compiler target and linker may accept the binary, but execution semantics still have to respect the host model. A Workers deployment cannot tolerate a Tokio integration that occasionally blocks the event loop or corrupts runtime context across reentrant calls.
A LocalEventLoop runtime for Tokio
The second approach is a more explicit event-loop integration. Cloudflare's post argues that event loops are familiar in native UI applications, so the idea was to design a Tokio runtime that could cooperate with a host event loop. The proposed design is a LocalEventLoop runtime: a local Tokio runtime whose wait behavior is replaced by a wake. Instead of parking a thread in an I/O driver, the runtime tells the host it has work, and the host calls drive() when it is ready.
The post breaks Tokio's runtime loop into two operations. First, the runtime polls tasks until nothing is ready. Second, it waits for I/O readiness, timers, or wakeups. In the LocalEventLoop model, polling becomes an explicit drive() call, and waiting becomes a host-owned std::task::Waker. Spawning a task returns immediately. When a future cannot make progress, control returns to the host. When a socket becomes readable, a timer expires, a task is woken, or another thread wakes work, Tokio uses the host waker to request that the runtime be driven again.
Cloudflare also describes an important constraint: block_on cannot behave as it does on a normal runtime. On LocalEventLoop, block_on runs a future as far as ready work allows it, but if it would need to park and wait for the host, it panics. The reason is structural: the host owns the wait, so the runtime cannot block inside the same call and still expect the host to wake it. The design favors explicit cooperation over hidden blocking.
The diagrams and examples in the Cloudflare post illustrate the contrast. A normal Tokio runtime can park in epoll_wait while handling a socket read. Under LocalEventLoop, the spawned task yields control back to the host, and the host interleaves its own event-loop work with Tokio's batches of ready tasks. Cloudflare frames this as a general architecture that could embed in GTK, Win32, Cocoa, Node.js, or browser-like environments, and notes that multiple such runtimes can coexist because of their cooperative execution semantics.
Sockets, epoll, and the Node.js compatibility bridge
Tokio support alone was not enough to make networked applications work. Cloudflare reports that most of the Tokio test suite could be supported after the runtime integration work, but the net feature remained unsupported because Emscripten provided poll() and a WebSocket emulation layer, not epoll_wait(). Tokio's I/O driver, via mio, is built around epoll-style readiness on the relevant path. Cloudflare wanted to integrate with Workers TCP socket APIs, including upcoming inbound TCP, which meant bridging Emscripten's virtualization layer to Workers' sockets layer.
The solution used an existing compatibility boundary. Workers already supports parts of the Node.js API surface, including node:net. Emscripten had -sNODERAWFS to bridge native-style file system calls to Node.js fs APIs. Cloudflare engineers realized they could use the same strategy for sockets rather than inventing a custom bridge. They contributed more than 40 pull requests to Emscripten, resulting in the -sNODERAWSOCKETS compilation option. According to the post, this option enables support for epoll, TCP, UDP, and Unix sockets for Emscripten applications in Node.js and, because Workers implements the same node:net API, on Workers as well.
The runtime details differ by integration mode. Under JSPI, epoll_wait() can suspend the stack until readiness, so Tokio's I/O driver behaves more like it would on a native platform. Under LocalEventLoop, readiness has to reach the host waker from a JavaScript callback. Cloudflare says it drafted an Emscripten proposal for emscripten_epoll_add_listener, which associates a callback with epoll ready events. The next drive() can then collect events with a zero-timeout epoll_wait(), allowing Tokio's existing I/O driver to be used without changing its core model.
This is an instructive systems-design pattern: when a platform already has a partial compatibility layer, extending that layer can be cheaper and more robust than building a private abstraction. The Workers runtime, Node.js compatibility APIs, Emscripten virtualization, and Tokio I/O driver each have their own constraints. The bridge works because the teams aligned those constraints around a shared interface instead of forcing application code to understand every layer.
The Pumpkin Minecraft server proof of concept
To stress-test the integration, Cloudflare describes a weekend project by Dan Lapid: running the Pumpkin Minecraft server inside a Durable Object. Pumpkin is a Rust Minecraft server built on Tokio and designed for multi-core machines. The post says world generation runs on a dedicated thread pool, while the game tick loop and chunk scheduler each run on their own OS threads. A Durable Object, by contrast, provides exactly one thread.
Porting Pumpkin therefore required turning threaded work into cooperative tasks on the event loop. Leaning on the Tokio integration, the tick loop and chunk scheduler became async tasks, and each Rayon job became a Tokio task. World generation ran on the event loop itself, taking one turn per chunk and interleaving with network I/O and game ticks rather than blocking them. Cloudflare's account is careful to describe the adaptation as a port that fits the Durable Object execution model; it does not claim that every multi-threaded server can be moved unchanged.
Persistence used Emscripten's -sNODERAWFS bridge. Pumpkin writes its world through ordinary std::fs; under -sNODERAWFS, those calls are forwarded to the node:fs compatibility layer. Cloudflare says Lapid's worker-fs-mount library, with its durable-object-fs backend, mounted a node:fs-compatible file system that stores files as rows in the Durable Object's SQLite storage. Each saved file becomes a row in the object's database, written synchronously and committed with the Durable Object's transaction. After restart, the object can boot from the same world.
Networking followed the same pattern of bridging rather than rewriting application logic. Player connections arrive through Workers TCP ingress at the Worker's connect() handler and are forwarded into the Durable Object. Inside the object, handleAsNodeConnection() from cloudflare:node dispatches the socket to a net.Server listening on that port. Because Emscripten's -sNODERAWSOCKETS implements TcpListener on top of net.Server, Pumpkin can accept players in much the same way it would on Linux. The returned promise indicates when the last player has left, allowing the object to save and shut down.
The Pumpkin example should be read as a capability demonstration. It shows that the Emscripten target can carry a non-trivial Rust application across several hard boundaries: async runtime integration, sockets, file persistence, and host-specific execution constraints. It does not, by itself, establish that running game servers in Durable Objects is the right production architecture for most workloads.
What developers should take from the preview
For Rust developers, the immediate value is the possibility of reusing more of the existing ecosystem on Workers. The Cloudflare post links to experimental example repositories for Emscripten Rust Workers, Tokio in a Worker, and TCP sockets with Emscripten and Tokio. Those examples are the right starting point because the feature is pre-release and depends on patchsets and evolving upstream support. Cloudflare explicitly describes the patchsets and workflows as available for experimental use.
The longer-term significance is architectural. Workers has historically rewarded code that fits the Web platform's event-driven model. That model is powerful, but it can make native-style Rust libraries awkward to adopt. Emscripten gives Rust code a familiar set of platform expectations, while wasm-bindgen preserves the ability to interoperate with JavaScript. The combination does not erase the differences between a browser-like isolate and a Unix process. It does provide a more capable translation layer for projects that need timers, files, sockets, or an async runtime.
There are also reasons for caution. The post repeatedly notes pre-release status, experimental patchsets, and ongoing upstream work, especially around Tokio's design for JSPI and event-loop integration. Developers evaluating the approach should test their dependency graphs, measure behavior under the Workers execution model, and avoid assuming that local native performance or threading behavior will transfer unchanged. Durable Objects are single-threaded, JavaScript hosts remain event-loop driven, and WebAssembly integration still has sharp edges.
Even with those caveats, the announcement is a meaningful step in Rust systems programming on managed edge platforms. It shows a path where Rust Workers are not limited to the subset of crates that already target browser-style WebAssembly. If the upstream work stabilizes, the Emscripten target could make Workers a more practical home for Rust applications that need Tokio, socket I/O, and existing native-oriented libraries, while keeping the deployment model inside Cloudflare's runtime.
Sources
- Cloudflare Blog: Supporting native Rust in Workers with the new Emscripten target for wasm-bindgen
- wasm-bindgen repository
- Cloudflare Workers Rust SDK examples: Emscripten
- Cloudflare Workers Rust SDK examples: Emscripten Tokio
- Cloudflare Workers Rust SDK examples: Emscripten TCP
- wasm-bindgen guide: Emscripten
- Emscripten: WASM_BINDGEN setting
- Emscripten: NODERAWSOCKETS setting
- Dan Lapid: Rust Workers Minecraft (Pumpkin on Durable Objects)
- worker-fs-mount