# Wasmer SDK: running real software in WebAssembly sandboxes

> The Wasmer SDK exposes one sandbox, package and process model to JavaScript, Python, Swift and Rust callers, so a Python interpreter, a PostgreSQL server or a PHP site can run inside a composable WebAssembly sandbox.

**wasmerio/wasmer-sdk** — Wasmer SDK: Run anything, anywhere suing WebAssembly-based sandboxes

- Repository: https://github.com/wasmerio/wasmer-sdk
- Website: https://wasmer.sh/
- Stars: 1,256 · Forks: 106
- Language: Rust
- License: NOASSERTION
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/wasmerio-wasmer-sdk

## What the Wasmer SDK adds on top of a WebAssembly runtime

The Wasmer SDK is a set of language bindings for the Wasmer runtime. The README states its purpose in one line: run real software inside lightweight WebAssembly sandboxes from JavaScript, Python, Swift, with Swift as a macOS preview, or from Rust.

What that means in practice is that the SDK does not only execute a module you compiled yourself. It resolves packages from the Wasmer registry, and the README lists what can be run that way: Python 3.13, PostgreSQL 18 with a native psql client, Edge.js, PHP, and shell tools, including a sandbox that holds several of these runtimes at once. The pitch is that a sandbox is composable: you hand it packages, files and capabilities, and you get an isolated workspace where you can run a command or keep a long-lived process under your control.

The alternative reading is more useful for judging it. This is a packaging and lifecycle layer over a runtime, not a new runtime. Every SDK in the family talks to the same Wasmer core, which is why the README can promise that the same package, sandbox, command, process, filesystem and port model is available in every language SDK. The repository is organised the same way, with js/, python/, rust/, swift/ and packages/ directories plus a fixtures/ directory that holds the guest programs all the examples share, so the JavaScript, Python and Rust examples run the same Edge.js server, the same Python program and the same PostgreSQL query.

## Creating a sandbox and running a command from JavaScript

The README opens with a JavaScript example that covers the whole loop: create a client, create a sandbox from a registry package, run a command in it, read the output.

```javascript
import { Wasmer } from "@wasmer/sdk/node";

const wasmer = new Wasmer();
const sandbox = await wasmer.sandboxes.create({
  packages: ["python/python@=3.13.20"],
  files: { "hello.py": "print('Hello from Wasmer')" },
});

const output = await sandbox
  .command("python", ["/workspace/hello.py"])
  .run();

console.log(output.text());
```

Three details carry the design. The package is pinned with the @=3.13.20 syntax, so a sandbox does not silently pick up a new Python release. The file is supplied as part of the sandbox definition rather than written afterwards. And the command is expressed as a program and an argument list, with the program name python, which is the name the package provides rather than a path into a filesystem image.

The path /workspace/hello.py also tells you where the supplied files land, and the same convention holds in the Python and Rust examples, which run the identical guest program from the shared fixtures directory.

## The sandbox object: files, ports, processes and late installs

A Wasmer client owns package resolution, workers, networking and the local cache, and wasmer.packages resolves reusable packages. Everything else belongs to a sandbox, and the README enumerates the members of that model.

Commands come in two shapes. command(...).run() captures a finite command and fails on an unsuccessful exit by default. command(...).spawn() starts a live process with stdin, stdout, stderr, termination and exit status, which is what you want for a server that stays up for the lifetime of the sandbox. sandbox.fs reads and writes the guest filesystem, sandbox.ports waits for guest services without inventing a separate server abstraction, and sandbox.installPackage() adds software after the sandbox has started.

The last item is the one that separates this from a simple command runner. A long-lived sandbox can have a package installed into it at runtime, so a service that needs a database client or an extra tool does not have to be described entirely at creation time.

There is deliberately no client-level run() shortcut. The README explains why: every execution has an explicit sandbox, which keeps package composition, files, capabilities and process lifetime visible. That is a design position rather than an oversight, and it is the reason the API does not read like the simpler wrappers people are used to.

## Exit status handling and the deliberate asymmetry

By default a finite command that ends with a non-zero status is an error, and run() propagates that. When a non-zero status is the expected outcome, JavaScript and Python callers can opt out, by passing check: false in JavaScript and check=False in Python.

The spawned-process side behaves differently on purpose. The README states that a spawned process wait() remains unchecked so applications can inspect every exit reason directly. A long-lived process can be terminated, can fail to start, or can end for a reason the caller wants to distinguish, and an unchecked wait leaves that classification to the application rather than collapsing it into an exception.

That asymmetry is worth knowing about before you port code between the SDKs. A script that treats a failed run() as an exception has to be rewritten, not just translated, when the same work moves to a spawned process.

## One API, four transports

The SDK family is unified at the API level but not at the transport level, and the README is explicit about how each binding reaches the runtime.

JavaScript runs Wasmer and WASIX directly in WebAssembly through wasm-bindgen, and Node networking is bridged through node:net and node:dns rather than a native addon. That last point matters for deployment, because there is no native module to compile for each platform. Python uses the Rust SDK through a Python-independent UniFFI library, and Swift uses that same UniFFI facade with an async Swift API. Rust uses Wasmer natively.

The install surface follows from that:

```bash
npm install @wasmer/sdk
```

```bash
pip install wasmer-sdk
```

The Rust binding is available as a workspace or from Git while crate publishing is disabled, and the Swift binding uses SwiftPM for macOS and iOS 27+. The Swift preview runs on macOS only, because the pinned runtime needs an iOS-capable backend before it can run on iPhone.

Each language has its own version, component tag and GitHub release with installable artifacts. npm and PyPI receive the same files attached to their respective GitHub releases, and the release process is described in docs/releases.md.

## Package caching and the .wasmer directory

Resolving a package means downloading it, and the SDK shares that work across languages. The default cache root is a .wasmer directory in the working directory. Native Rust, Python and Node.js clients share the same registry metadata and downloaded package data, while native compiled artifacts live in target specific cache partitions and the JavaScript client keeps its runtime specific data separate.

The split is deliberate. Registry metadata and package contents are portable between the native clients, so a Python process and a Rust process on the same machine download a package once. Compiled artifacts are not portable in the same way, because they depend on the target and the runtime, so they are partitioned instead of shared.

The cache root can be pointed elsewhere when an application needs a different location, and the README advises committing neither .wasmer nor the compiled artifacts to source control. For a CI system that runs the same package across several test jobs, that default location is also the thing to configure, since a fresh checkout otherwise means a fresh download.

## Alpha status, the pinned runtime and where the SDK is the wrong tool

The README states that the SDK is currently alpha, that the cross-language shape is intentional, and that error codes and less common capabilities may still evolve. The architectural rationale sits in a design document, docs/warp-universal-wasmer-sdk.md.

The runtime is pinned rather than tracked loosely. The workspace Cargo.toml patches a group of Wasmer crates to a single revision of the wasmer repository, including wasmer, wasmer-wasix, wasmer-types, wasmer-config, virtual-fs, virtual-mio and virtual-net, and it resolves the NAPI crates the same way. That pinning is what makes the shared cache and the identical behaviour across languages possible, and it is also the reason an upgrade is a coordinated change rather than a version bump.

The workspace itself is small:

```toml
[workspace]
members = [
    "js/bindgen",
    "rust",
    "rust/uniffi",
]
default-members = ["rust"]
```

So the SDK is the wrong tool in two situations. If you only need to run a WebAssembly module you built yourself, with no registry packages, no guest filesystem to manage and no long-lived processes, the raw Wasmer runtime is a smaller dependency and a smaller surface. And if you need a specific service version, the sandbox is created by resolving a named package with a version constraint, so a package that does not exist in the registry cannot be substituted by the SDK; you would have to build a package yourself.

There is a third constraint. The Swift binding is a preview, the Rust crate is not published to crates.io, and the whole thing is alpha, so a project that treats error handling as a contract across languages is committing to an API the README says may still change.

## Conclusion

The Wasmer SDK fits a team that needs to run untrusted or heavy software, such as a Python program or a PostgreSQL instance, from several language stacks against one API, and the shared fixtures directory makes cross-language behaviour comparable. It does not fit a project that only needs to run a WebAssembly module it controls, where the raw Wasmer runtime is simpler. Check first whether the registry package you need exists, since sandbox creation resolves packages by name and version.

## FAQ

### How do I install the Wasmer SDK for JavaScript and Python?

Use npm install @wasmer/sdk for JavaScript and pip install wasmer-sdk for Python. The Rust binding is available as a workspace or from Git while crate publishing is disabled, and Swift uses SwiftPM for macOS and iOS 27+.

### How do I run a command inside a Wasmer sandbox?

Create a sandbox with wasmer.sandboxes.create(), giving it packages, files and other settings, then call sandbox.command("python", ["/workspace/hello.py"]).run() to capture the output. A live process uses spawn() instead, with stdin, stdout, stderr and exit status.

### How do I handle a command that is expected to exit with an error?

JavaScript and Python callers can pass check: false or check=False so a non-zero status is not treated as a failure. A spawned process wait() is always unchecked so the application can inspect the exit reason directly.

### Where does the Wasmer SDK cache downloaded packages?

In a .wasmer directory in the working directory by default. Native Rust, Python and Node.js clients share the registry metadata and package data, native compiled artifacts are kept in target specific partitions, and the JavaScript client keeps its runtime specific data separate.

## Sources

- [Issues](https://github.com/wasmerio/wasmer-sdk/issues)
- [Project website](https://wasmer.sh/)
- [README](https://github.com/wasmerio/wasmer-sdk/blob/main/README.md)
- [Releases](https://github.com/wasmerio/wasmer-sdk/releases)
- [wasmerio/wasmer-sdk on GitHub](https://github.com/wasmerio/wasmer-sdk)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/wasmerio-wasmer-sdk
