cunarist/tokio-with-wasm

Mimicking tokio functionalities on web browsers

Rust

157

265 commits

updated Aug 13, 2026

See the code

README

About this library

Crates.io Documentation License

Example

tokio_with_wasm is a Rust library that provides tokio for web browsers. It aims to offer the exact same tokio API to Rust web applications.

This library is made up of JavaScript glue code that mimics the behavior of real tokio. tokio_with_wasm doesn't have its own runtime and adapts to the JavaScript event loop.

When using spawn_blocking(), the number of web workers is automatically adjusted to the number of parallel tasks. Refer to the docs for additional details.

This library assumes that you're compiling your Rust project with wasm-pack and wasm-bindgen, which build for the wasm32-unknown-unknown and wasm64-unknown-unknown Rust targets. Note that this library only supports the web target of wasm-bindgen, not others such as no-modules.

Features

  • Familiar API: If you're familiar with tokio, you'll feel right at home with tokio_with_wasm. It provides similar functionality and follows the same patterns for spawning and managing asynchronous tasks.

  • Web worker integration: tokio_with_wasm adapts to the JavaScript environment by utilizing web APIs under the hood. This means you can write Rust code that runs concurrently and efficiently in web applications.

  • Spawn async and blocking tasks: You can spawn both asynchronous and blocking tasks. Asynchronous tasks allow you to perform non-blocking operations, while blocking tasks are suitable for compute-heavy or synchronous tasks.

  • File system: fs reads and writes files in the OPFS, the store every browser keeps on disk for one origin.

net, process, and signal have no counterpart on the web, so they are missing from the web build. Using them is a compile error, not a runtime failure.

Use cases

  • Single-page apps: heavy computation without freezing the UI.
  • Browser extensions: parallel Rust under Manifest V3's strict security policy.
  • In-browser tools: parsing, encoding, and image processing on real threads.
  • Games and simulations: physics and AI on workers while the main thread renders.
  • Local inference: ML models on a worker while the page stays responsive.

Usage

Add this library to your Cargo.toml alongside tokio:

[dependencies]
tokio = { version = "0.0.0", features = ["macros", "sync", "time", "rt"] }
tokio_with_wasm = { version = "0.0.0", features = ["macros", "sync", "time", "rt"] }

Keep the feature lists of the two dependencies in sync. tokio's features serve native platforms, and tokio_with_wasm's features enable the web glue.

Here's a simple example of using tokio_with_wasm that works on both native platforms and web browsers:

use tokio::task::{spawn, spawn_blocking, yield_now, JoinSet};
use tokio::time::{interval, sleep};
use tokio_with_wasm::alias as tokio;

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let async_join_handle = spawn(async {
    // Asynchronous code here.
    // This will run concurrently
    // in the same web worker (thread).
  });
  let blocking_join_handle = spawn_blocking(|| {
    // Blocking code here.
    // This will run in parallel
    // in the external pool of web workers.
  });
  let async_result = async_join_handle.await;
  let blocking_result = blocking_join_handle.await;
  for i in 1..=1000 {
    // Some repeating task here
    // that shouldn't block the JavaScript runtime.
    yield_now().await;
  }
}

The use tokio_with_wasm::alias as tokio; statement is functionally equivalent to the code below. This import is provided for convenience, allowing for shorter code.

#[cfg(all(
  target_family = "wasm",
  target_vendor = "unknown",
  target_os = "unknown"
))]
use tokio_with_wasm as tokio;

#[cfg(not(all(
  target_family = "wasm",
  target_vendor = "unknown",
  target_os = "unknown"
)))]
use tokio;

Documentation

API documentation can be found on docs.rs.

Caution

Never panic!

Stick to the Result enum whenever possible.

On wasm32-unknown-unknown, there's currently no way to catch and unwind panics like on native platforms. Panics will eventually lead to leaked JavaScript Promises.

A panic inside spawn_blocking takes down the web worker that runs it. The JoinHandle resolves to a JoinError whose is_panic is true, but the panic payload is lost and the worker's share of the shared memory is never reclaimed.

Threads are actually web workers

If you're using Web Workers (threads) by calling spawn_blocking, you need to set specific Rust compiler flags. Also, you must use the nightly toolchain and include certain Rust standard library components in the compilation.

  • target-feature flags
    • +atomics
    • +bulk-memory
    • +mutable-globals
  • link-arg flags
    • --shared-memory
    • --max-memory=1073741824
    • --import-memory
    • --export=__wasm_init_tls
    • --export=__tls_size
    • --export=__tls_align
    • --export=__tls_base
  • build-std components
    • std
    • panic_abort

Here's a full example command:

export RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals -C link-arg=--shared-memory -C link-arg=--max-memory=1073741824 -C link-arg=--import-memory -C link-arg=--export=__wasm_init_tls -C link-arg=--export=__tls_size -C link-arg=--export=__tls_align -C link-arg=--export=__tls_base"
export RUSTUP_TOOLCHAIN="nightly"
wasm-pack build <path> --target web -- -Z build-std=std,panic_abort

After building your WebAssembly module and preparing it for deployment, ensure that your web server is configured to include cross-origin-related HTTP headers in its responses. These headers let clients of your website access the SharedArrayBuffer web API, which is the web's counterpart to shared memory.

Serve .wasm with the right MIME type

Don't forget to specify the MIME type application/wasm for .wasm files in your HTTP server configuration to ensure optimal performance.

WebAssembly.instantiateStreaming() rejects any other MIME type, so the module falls back to being compiled after the whole download finishes rather than while it arrives.

Adapt to a strict CSP

spawn_blocking runs its web workers from a blob: script by default. Under a content security policy that forbids blob: workers, such as a browser extension's script-src 'self', serve blocking_worker.js as your own file and point the pool at it:

tokio_with_wasm::only_web::set_worker_script_provider(|| Ok("/blocking_worker.js".into()));

Why this is needed

The web has many restrictions due to its sandboxed environment, which prevents the use of threads, time, file IO, network IO, and many other native functionalities. Consequently, certain features are missing from Rust's std. That's why tokio doesn't really work well in web browsers.

To address this issue, this crate offers tokio modules with the same names as the original native ones, providing workarounds for these constraints.

Future vision

Because a large portion of Rust's web ecosystem is based on wasm32-unknown-unknown right now, we had to make an alias crate of tokio to use its functionalities directly on the web.

Hopefully, when wasm32-wasi becomes the mainstream Rust target for the web, jco might be an alternative to wasm-bindgen as it can provide full std functionalities with browser shims (polyfills). However, this will take time because the wasi-threads proposal still has a long way to go.

Until that time, there's tokio_with_wasm!

Contribution guide

Contributions are always welcome! If you have any suggestions, bug reports, or want to contribute to the development of tokio_with_wasm, please open an issue or submit a pull request.

There are situations where you cannot use native Rust code directly on the web. This is because the wasm32-unknown-unknown Rust target used by wasm-bindgen doesn't have a full std module. Refer to the links below to understand how to interact with JavaScript with wasm-bindgen.

Rust code can be called in a web worker. Therefore, we cannot access the global window JavaScript object as we can on the main thread of JavaScript. Refer to the link below to check which web APIs are available in a web worker. You'll be surprised by the various capabilities of modern JavaScript.

Please note that this library uses quite a hacky and naive approach to mimic native tokio functionalities. That's because this library is meant as a temporary solution for the period before wasm32-wasi. Any kind of PR is welcome, as long as it makes things just work on the web.

async
browser
tokio
wasm

Contributors

temeddix

252 commits

Slixe

11 commits

hansSchall

1 commits

namse

1 commits

cunarist/tokio-with-wasm

Mimicking tokio functionalities on web browsers

Rust

157

265 commits

updated Aug 13, 2026

See the code

README

About this library

Crates.io Documentation License

Example

tokio_with_wasm is a Rust library that provides tokio for web browsers. It aims to offer the exact same tokio API to Rust web applications.

This library is made up of JavaScript glue code that mimics the behavior of real tokio. tokio_with_wasm doesn't have its own runtime and adapts to the JavaScript event loop.

When using spawn_blocking(), the number of web workers is automatically adjusted to the number of parallel tasks. Refer to the docs for additional details.

This library assumes that you're compiling your Rust project with wasm-pack and wasm-bindgen, which build for the wasm32-unknown-unknown and wasm64-unknown-unknown Rust targets. Note that this library only supports the web target of wasm-bindgen, not others such as no-modules.

Features

  • Familiar API: If you're familiar with tokio, you'll feel right at home with tokio_with_wasm. It provides similar functionality and follows the same patterns for spawning and managing asynchronous tasks.

  • Web worker integration: tokio_with_wasm adapts to the JavaScript environment by utilizing web APIs under the hood. This means you can write Rust code that runs concurrently and efficiently in web applications.

  • Spawn async and blocking tasks: You can spawn both asynchronous and blocking tasks. Asynchronous tasks allow you to perform non-blocking operations, while blocking tasks are suitable for compute-heavy or synchronous tasks.

  • File system: fs reads and writes files in the OPFS, the store every browser keeps on disk for one origin.

net, process, and signal have no counterpart on the web, so they are missing from the web build. Using them is a compile error, not a runtime failure.

Use cases

  • Single-page apps: heavy computation without freezing the UI.
  • Browser extensions: parallel Rust under Manifest V3's strict security policy.
  • In-browser tools: parsing, encoding, and image processing on real threads.
  • Games and simulations: physics and AI on workers while the main thread renders.
  • Local inference: ML models on a worker while the page stays responsive.

Usage

Add this library to your Cargo.toml alongside tokio:

[dependencies]
tokio = { version = "0.0.0", features = ["macros", "sync", "time", "rt"] }
tokio_with_wasm = { version = "0.0.0", features = ["macros", "sync", "time", "rt"] }

Keep the feature lists of the two dependencies in sync. tokio's features serve native platforms, and tokio_with_wasm's features enable the web glue.

Here's a simple example of using tokio_with_wasm that works on both native platforms and web browsers:

use tokio::task::{spawn, spawn_blocking, yield_now, JoinSet};
use tokio::time::{interval, sleep};
use tokio_with_wasm::alias as tokio;

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let async_join_handle = spawn(async {
    // Asynchronous code here.
    // This will run concurrently
    // in the same web worker (thread).
  });
  let blocking_join_handle = spawn_blocking(|| {
    // Blocking code here.
    // This will run in parallel
    // in the external pool of web workers.
  });
  let async_result = async_join_handle.await;
  let blocking_result = blocking_join_handle.await;
  for i in 1..=1000 {
    // Some repeating task here
    // that shouldn't block the JavaScript runtime.
    yield_now().await;
  }
}

The use tokio_with_wasm::alias as tokio; statement is functionally equivalent to the code below. This import is provided for convenience, allowing for shorter code.

#[cfg(all(
  target_family = "wasm",
  target_vendor = "unknown",
  target_os = "unknown"
))]
use tokio_with_wasm as tokio;

#[cfg(not(all(
  target_family = "wasm",
  target_vendor = "unknown",
  target_os = "unknown"
)))]
use tokio;

Documentation

API documentation can be found on docs.rs.

Caution

Never panic!

Stick to the Result enum whenever possible.

On wasm32-unknown-unknown, there's currently no way to catch and unwind panics like on native platforms. Panics will eventually lead to leaked JavaScript Promises.

A panic inside spawn_blocking takes down the web worker that runs it. The JoinHandle resolves to a JoinError whose is_panic is true, but the panic payload is lost and the worker's share of the shared memory is never reclaimed.

Threads are actually web workers

If you're using Web Workers (threads) by calling spawn_blocking, you need to set specific Rust compiler flags. Also, you must use the nightly toolchain and include certain Rust standard library components in the compilation.

  • target-feature flags
    • +atomics
    • +bulk-memory
    • +mutable-globals
  • link-arg flags
    • --shared-memory
    • --max-memory=1073741824
    • --import-memory
    • --export=__wasm_init_tls
    • --export=__tls_size
    • --export=__tls_align
    • --export=__tls_base
  • build-std components
    • std
    • panic_abort

Here's a full example command:

export RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals -C link-arg=--shared-memory -C link-arg=--max-memory=1073741824 -C link-arg=--import-memory -C link-arg=--export=__wasm_init_tls -C link-arg=--export=__tls_size -C link-arg=--export=__tls_align -C link-arg=--export=__tls_base"
export RUSTUP_TOOLCHAIN="nightly"
wasm-pack build <path> --target web -- -Z build-std=std,panic_abort

After building your WebAssembly module and preparing it for deployment, ensure that your web server is configured to include cross-origin-related HTTP headers in its responses. These headers let clients of your website access the SharedArrayBuffer web API, which is the web's counterpart to shared memory.

Serve .wasm with the right MIME type

Don't forget to specify the MIME type application/wasm for .wasm files in your HTTP server configuration to ensure optimal performance.

WebAssembly.instantiateStreaming() rejects any other MIME type, so the module falls back to being compiled after the whole download finishes rather than while it arrives.

Adapt to a strict CSP

spawn_blocking runs its web workers from a blob: script by default. Under a content security policy that forbids blob: workers, such as a browser extension's script-src 'self', serve blocking_worker.js as your own file and point the pool at it:

tokio_with_wasm::only_web::set_worker_script_provider(|| Ok("/blocking_worker.js".into()));

Why this is needed

The web has many restrictions due to its sandboxed environment, which prevents the use of threads, time, file IO, network IO, and many other native functionalities. Consequently, certain features are missing from Rust's std. That's why tokio doesn't really work well in web browsers.

To address this issue, this crate offers tokio modules with the same names as the original native ones, providing workarounds for these constraints.

Future vision

Because a large portion of Rust's web ecosystem is based on wasm32-unknown-unknown right now, we had to make an alias crate of tokio to use its functionalities directly on the web.

Hopefully, when wasm32-wasi becomes the mainstream Rust target for the web, jco might be an alternative to wasm-bindgen as it can provide full std functionalities with browser shims (polyfills). However, this will take time because the wasi-threads proposal still has a long way to go.

Until that time, there's tokio_with_wasm!

Contribution guide

Contributions are always welcome! If you have any suggestions, bug reports, or want to contribute to the development of tokio_with_wasm, please open an issue or submit a pull request.

There are situations where you cannot use native Rust code directly on the web. This is because the wasm32-unknown-unknown Rust target used by wasm-bindgen doesn't have a full std module. Refer to the links below to understand how to interact with JavaScript with wasm-bindgen.

Rust code can be called in a web worker. Therefore, we cannot access the global window JavaScript object as we can on the main thread of JavaScript. Refer to the link below to check which web APIs are available in a web worker. You'll be surprised by the various capabilities of modern JavaScript.

Please note that this library uses quite a hacky and naive approach to mimic native tokio functionalities. That's because this library is meant as a temporary solution for the period before wasm32-wasi. Any kind of PR is welcome, as long as it makes things just work on the web.

async
browser
tokio
wasm

Contributors

temeddix

252 commits

Slixe

11 commits

hansSchall

1 commits

namse

1 commits

Languages

Rust

98.2%

JavaScript

1.5%