Skip to content
Cap'n Web

One round trip!

A JavaScript-native, object-capability RPC system. Chain dependent calls and the whole chain resolves in a single trip. No schemas, no boilerplate, under 16 kB.

Two sequence diagrams side by side on one shared time axis, on a 100 millisecond link where each call takes the server 10 milliseconds to handle. Without Cap'n Web, four dependent calls are awaited one at a time, so each pays for its own round trip and its own visit to the server: eight crossings, four handlers, 440 milliseconds. With Cap'n Web the same four calls are pipelined into one batched request, and the far end runs all four handlers before answering once: two crossings, the same four handlers, 140 milliseconds. The server does identical work in both; the 300 milliseconds saved is the waiting that is gone.

Cap’n Web is a spiritual sibling to Cap’n Proto, created by the same author, but designed to play nice in the web stack. Like Cap’n Proto it is an object-capability protocol: “Cap’n” is short for “capabilities and”. Unlike Cap’n Proto, it has no schemas, almost no boilerplate, and its serialization is just JSON with a little pre- and post-processing.

Why it’s different

FIG. 01

Promise pipelining

Don't await a result before you use it. Chain dependent calls together and the whole chain resolves in a single network round trip — even over plain HTTP.

FIG. 02

Bidirectional by default

Sessions are symmetric: the client can call the server, and the server can call the client. Pass a function and the other side gets a stub that calls back.

FIG. 03

Pass by reference

Classes that extend RpcTarget travel as references, not copies. You hold a stub; method calls run where the object actually lives.

FIG. 04

No schemas, no codegen

No .proto files, no build step, no generated clients. Types are just TypeScript — erased at runtime, and free.

FIG. 05

Runs everywhere

Every major browser, Cloudflare Workers, Node, Deno, and Bun — over HTTP, WebSocket, MessagePort, or a transport you write yourself.

FIG. 06

under 16 kB

Minified and gzipped, with zero dependencies. The whole protocol is human-readable JSON — you can read it straight from the network tab.

The magic trick

Every call returns an RpcPromise. You can use that promise, or a property of it, as the input to another call before it has resolved. The server substitutes the real value on arrival, so a chain of dependent calls costs exactly one round trip:

// Authenticate, get the user's ID, fetch their profile, and
// fetch every friend's profile too. One request, one response.
let authed = api.authenticate(apiToken);
let profile = api.getUserProfile(authed.getUserId());
let friends = authed.getFriendIds().map(id => api.getUserProfile(id));

let [me, myFriends] = await Promise.all([profile, friends]);

See how promise pipelining works → — a step-by-step tour, and the record-replay trick behind .map().

See it running

Both examples run right here in the docs, with the source alongside them. They make the same point from opposite ends of the stack: the pipelined version issues one HTTP request where the ordinary version issues three.

Get going

Type to search…

↑↓ navigate↵ selectEsc close