← Blog

Oam.js: a runtime for TypeScript and MCP servers, with the denominators attached

August 8, 2026

Oam.js (oam) is a JavaScript/TypeScript runtime written in Rust on V8. It runs TypeScript directly, including .tsx/.jsx, with no build step and no loader hook, and it is built to host MCP servers — many short-lived processes, idle most of the time, each needing to answer its first request fast.

It is beta. That word is doing real work here, and the specifics are near the end.

The thing worth arguing about is not speed. It is that every number below comes with the denominator it was computed against, and with the cases where another runtime wins. A percentage without a denominator is a marketing asset, not a measurement.

Conformance: 429/431, and what that ratio is over

A vendored subset of Node's own test suite runs on every release. The current scorecard (CONFORMANCE-NODE.md, generated at commit 869aa9e, oam 0.9.0 against Node v22.22.2):

Now the qualifications, from that scorecard and docs/node-divergences.md:

That is pass over tests that RAN, not pass over tests that exist. Against every vendored file on Windows the same run is 429/476 (90.1%) — 429 pass, 2 fail, 22 skip at runtime, 23 unrunnable by the harness. Most of the unrunnable set needs a node:internal/* module oam does not have, a large share of those being internal/test/binding, Node's C++ test hooks. Inventing those modules to make the ratio look better would be fabrication.

The corpus is a subset of Node's suite. It covers assert, buffer, events, path, process, querystring, stream, string_decoder, timers, url, and util. The socket- and fixture-heavy modules — fs, net, http, child_process, tls — land in later tranches, and the percentage will move when they do.

Some skips are not oam gaps. Real Node, run against the same vendored tree on the same host, skips several of them identically (POSIX-only tests on Windows, styletext with no TTY). Windows also runs the fewest tests of the three platforms, so its ratio alone is not the picture — the POSIX hosts carry larger denominators because they run the tests Windows skips.

The oracle is exit 0. Node core tests self-assert, so a test "passes" when it runs to completion without throwing. That is Node's own bar, but it is coarser than a golden-output diff. The stricter check is the Node differential suite in CONFORMANCE.md: 55/55 cases with byte-identical stdout against a live node v22.22.2. WPT URL data is 888/888 on the constructor suite and 278/278 on setters.

Both remaining failures on every platform are deliberate, and one is permanent. test-process-versions.js fails because process.versions refuses to publish version strings for libraries oam does not contain — no uv, no openssl, no llhttp. It publishes oam's real dependency tree under real names (tokio, hyper, reqwest, rustls, ring, oxc, and the rest), read from Cargo.lock at build time so it cannot drift from what is linked. The sharp case is modules/napi: addon loaders read those to decide a precompiled .node binary is loadable. oam cannot load one, so the polite lie would turn a package's working JS fallback into a crash.

Benchmarks: Bun wins three of the nine cases, Node one

The committed table in BENCHMARKS.md is generated by cargo run -p xtask -- bench --release --compare at commit acec008, release profile, host windows-aarch64, and its header records the runtimes as oam 0.9.0, node v22.22.2, bun 1.3.14.

These numbers replace an earlier table that was wrong twice over. It was measured on a build old enough to still report oam 0.0.1, and its mcp-idle-rss column was produced by a harness bug: the Windows parser split tasklist output on ,, which cut the memory field at its own thousands separator, and rejoined the halves backwards. It read 40.65MB where the real figure is 25.23MB — understating oam's own advantage. One machine's numbers are still one machine's numbers, so re-run them on yours.

All times in milliseconds, lower is better. mcp-idle-rss is MB.

Caseoamnodebun
cold-start59.58113.42307.97
url-parse6.539.305.43
http-throughput47.78107.7533.98
fs-read30.0528.9138.73
json-parse74.50115.9991.15
crypto-hash223.89292.2938.99
mcp-cold-start35.00250.87483.19
mcp-idle-rss25.2361.3498.43
mcp-first-call-latency0.342.405.04

Bun is faster than oam at url-parse, http-throughput and crypto-hash on the same hardware. On crypto-hash it is not close: 38.99ms against oam's 223.89ms. If raw single-process throughput is what you are optimising, benchmark Bun and you will probably pick Bun. Node edges oam on fs-read (28.91ms against 30.05ms) — close enough that it is a tie in practice, and the honest reading is that oam has no advantage there.

oam's case is the other shape. mcp-cold-start is 35.00ms against Bun's 483.19ms and Node's 250.87ms; mcp-idle-rss 25.23MB against 98.43MB and 61.34MB; mcp-first-call-latency 0.34ms against 5.04ms and 2.40ms. A broker that starts a dozen sidecars pays each of those costs a dozen times, and then holds the RSS for as long as the sidecars live.

One methodology caveat: in mcp-cold-start oam uses its built-in oam:mcp virtual module while Node and Bun load @modelcontextprotocol/sdk (same noop tool, same stdio transport). That measures what it costs to start an MCP server on each runtime, not the runtimes in isolation — part of oam's win is that the protocol surface ships in the binary instead of being installed from npm.

What will bite you

When to pick something else

Node, if you need native addons, the full ecosystem surface, or you are operating a long-lived production service today. Deno, if you want its standard library, deno deploy, or its module ecosystem — those are not goals here. Bun, if single-process throughput is the axis you are optimising, or you want the bundler, test runner and package manager in the same binary.

oam is for running TypeScript and MCP servers. If that is the workload, the argument is the table above and the denominators under it. If it is not, one of the other three is the better tool, and docs/why-oam.md says which.

License is Apache-2.0, forever. The scorecards and the benchmark table are generated by the CI gate and never hand-edited, which is the only reason it is safe to quote them here.