capnwasm vs capnweb

Where capnwasm wins vs capnweb vs REST/JSON, where it loses, where it’s tied. Bundle and fixture-wire rows load build-generated numbers from the current assets; runtime rows are benchmark snapshots with links to the live benches.

Where capnwasm wins

workloadcapnwasmcapnwebREST/JSONwin
Burst 1000 calls (per-call) measuring… measuring… n/a live
64 KB text echo round-trip measuring… measuring… n/a live
4 KB text echo measuring… measuring… n/a live
Single tiny call measuring… measuring… n/a live
Wire bytes for 4 KB binary blob (gzip) loading… loading… loading… build metric
Sparse field access (read 3 of 32) measuring… measuring… live

capnwasm consistently beats capnweb on:

Where capnwasm loses

These are real, not handwave-able-away.

1. Bundle size: ~2× larger on the wire (apples-to-apples brotli)

gzip (Workers deploy limit)brotli (browsers / Cloudflare Pages)
capnweb (everything)loading…loading…
capnwasm wasm-only (decode capnp messages)loading…loading…
capnwasm + RPCloading…loading…
capnwasm typed + http-batch (typical browser app)loading…loading…

Loading build metrics…

If your bundle budget is tight and you don't need binary wire interop, capnweb is the smaller choice. There is no way for capnwasm to reach 21 KB while still doing RPC; the wasm runtime is what enables the rest of the wins.

2. Cold start: now essentially tied in Node, still slower in the browser

importload (compile + link)total
capnweb1.5 ms0.4 ms1.9 ms
capnwasm (inlined)1.0 ms0.8 ms1.8 ms
capnwasm (slim, separate wasm)0.4 ms0.6 ms1.0 ms

Numbers are fresh-process Node 22, average of 8 runs. In an earlier release capnwasm's init was ~11 ms because the loader did instanceof Response, which lazy-initializes Node's undici fetch implementation. The fix is duck-typing: never reach for Response unless the source is clearly a URL.

In a browser the picture is different. With an empty HTTP cache the first visit pays network + streaming compile; the landing page measures the current browser/host directly. Warm reloads (V8 code-cache hit) drop close to the measurement floor. capnweb's smaller JS parses quickly either way. So on a fresh tab capnwasm is still measurably slower because the wasm bytes are larger; once the bundle is cached the gap is small.

3. Schema friction

capnwasm requires a Cap'n Proto schema — even when generating a TypeScript-only client you go through npx capnwasm gen. capnweb works on arbitrary JS values: you call methods, JSON-shaped data goes over the wire, no schema step.

If you control both ends and never want to define a schema, capnweb is friction-free. If you want types, IDE completion, wire-format stability across versions, and interop with C++/Rust/Go peers, the schema is the price you pay for that.

4. Cap-passing micro-overhead

capnwasmcapnweb
getChild → call round-tripmeasuring…measuring…

Roughly tied — capnwasm is ~5% slower on the cap-passing fast path. Doesn't matter unless you're doing very deep cap chains; both are dominated by network RTT in any real deployment.

5. Single-call latency over real network: invisible

The 8.5 μs vs 14 μs gap on tiny calls only matters if the network adds < 5 μs of latency, which essentially never happens. Over a real WebSocket on the same continent (typically 5–50 ms RTT), you cannot tell capnwasm and capnweb apart on a single tiny call.

The win shows up in bursts (parallel calls) and payload size (decode/encode work).

Where the in-browser playground says they're tied

The live playground on this site fetches N records as same-origin static assets and renders them. When network cost is near-free, the three protocols come out within a few percent of each other:

workloadRESTcapnwebcapnwasmcapnp wire (gz)
200 records, 32 B avatars107 ms108 ms110 ms26.6 KB
50 records, 4 KB avatars15 ms15 ms15 ms205 KB (vs 272 KB JSON)

The bytes-on-wire savings only translate into faster end-to-end times once the network has any meaningful latency. In a same-origin static-fixture bench, fetch overhead dominates everything else — capnwasm's decoder is fast, but it can't pay back the wasm load + per-record boundary cost when network is essentially free. Add real latency between you and your backend and the bytes savings start to compound.

Where the playground can't show capnwasm winning yet: an RPC workload over WebSocket with bursts, capability passing, or 64 KB binary payloads. That's a follow-up bench page.

Where REST/JSON loses to both

For completeness — neither library is competing with raw fetch() on the protocol axis, but it's worth showing what JSON-without-RPC costs:

If your app fits inside “fetch some data, render it, occasionally POST” then plain REST is fine and probably what you should use. Both capnweb and capnwasm are for apps that need RPC semantics — capability-secure objects, pipelining, streaming, wire-compatible types.

When to choose what

Choose REST/JSON when:

Choose capnweb when:

Choose capnwasm when:

End-to-end with rendering

Per-call latency is one slice of the truth. The full picture is what happens from cap.method() through wire, decode, every .field read used by render, to forced layout. We built the live render bench to measure exactly that across capnweb × capnwasm × (WebSocket, HTTP-batch) × (small, medium, large) × (cold, warm). Every cell. No averages.

Snapshot from Apple Silicon, Chromium prod build, same-origin local run (warm medians, 10 iter). For current numbers, use the live render bench:

capnwasm wins (warm medians)

workloadcapnwasm/WScapnweb/WSnote
Binary blob 256 KB round-trip1.0 ms2.3 ms2.3× faster — raw bytes vs base64
Sparse field access (50× calls, read 3 of 32)2.6 ms3.2 msonly crosses wasm 3× per call, capnweb materializes all 32
List render 100 records1.2 ms1.4 msschema-typed binary decode beats JSON.parse + DOM

capnweb wins (warm medians)

workloadcapnweb/WScapnwasm/WSnote
Re-read storm (1000× field reads after one fetch)200 μs400 μs2× faster — pure JS reads vs per-read wasm crossing
List render 1000 records8.9 ms9.7 msDOM dominates; capnweb's up-front parse is “free” here

Practically tied (warm medians)

workloadcapnwasm/WScapnweb/WSnote
Dense field access (50× calls, read all 32)14.9 ms13.8 mswithin noise; capnwasm crosses wasm 32× per call, capnweb's eager-decode catches up
Re-read 10×200 μs300 μsboth at the measurement floor on a hot WS

HTTP-batch numbers generally trail WS for both libraries because each request reopens / dispatches a session. The cold path for HTTP is shorter than WS (no handshake), but the warm path is longer (no kept connection). capnwasm holds the lead on blobs and sparse reads even over HTTP; capnweb keeps its re-read advantage on either transport.

Cold paths are all over the place for sub-100-ms workloads. The first call after opening a transport mixes WS handshake / first POST / wasm fetch (capnwasm only) / V8 JIT warmup, none of which is the library's fault. Look at warm.

The honest takeaway: pick capnwasm if your workload is binary-heavy, sparse-field, or you want one schema across languages. Pick capnweb if your workload is JS-only and dominated by repeated reads of small payloads. The render bench is a click away — don't take our word for it.

Methodology / reproducibility