Cloudflare — Cap'n Web: a new RPC system for browsers and web servers¶
Summary¶
Announcement and full design walkthrough for
Cap'n Web, a new RPC protocol and pure-TypeScript implementation
open-sourced by Cloudflare (MIT, github.com/cloudflare/capnweb
). Written by Kenton
Varda, author of Cap'n Proto, and
positioned as a "spiritual sibling" that keeps Cap'n Proto's
object-capability RPC model
and promise pipelining while
dropping the schema language and binary wire format in favour of
plain TypeScript interfaces and a JSON-based human-readable
wire format. The library compresses (minify + gzip) to under
10 kB with no dependencies, runs in every major browser + Cloudflare
Workers + Node.js + other modern JavaScript runtimes, and ships
transports for HTTP batch, WebSocket, and postMessage()
out of the box. First production consumer is Wrangler's remote
bindings feature (GA 2025-09-16), which uses Cap'n Web so a local
workerd test instance can speak RPC to services in production.
The post also pitches Cap'n Web as a GraphQL alternative for
the "waterfall" problem — solving it by pipelining calls at the
RPC layer instead of introducing a new query language, and
extending the solution to .map() over promised arrays via a
novel record-replay DSL.
Key takeaways¶
-
Object-capability RPC, in TypeScript, with no schemas. The "Cap'n" in Cap'n Proto / Cap'n Web stands for "capabilities and …". Both protocols treat functions and objects as first-class references over the wire; the recipient of a function/object reference gets a stub whose invocations route back to the origin. Cap'n Web keeps this model but removes the
.capnpschema language + codegen step — you declare an API as a plain TypeScript interface, implement it by extending a marker classRpcTarget, and the client auto-gets end-to-end type safety at compile time with no generated code. (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers) -
The four-message protocol. The wire protocol has four message types:
push(evaluate an expression and store the result in the peer's export table at a predictable positive ID),pull(please serialize and return the value at this ID — issued only if the caller actuallyawaits),resolve/reject(here's the value, or the call threw). Each peer maintains import + export tables indexed by signed integer IDs — positive for pushes (predictable by the caller → enables pipelining), negative for pass-by-reference objects/functions in outgoing messages (starting at -1 and counting down). "IDs are never reused over the lifetime of a connection." (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers) -
Promise pipelining is enabled by predictable export IDs. Because the caller can predict the positive ID each
pushwill receive, it can immediately use that ID in subsequent messages — telling the peer "invoke method X on the result of my earlier push" before the earlier push has even been evaluated, let alone returned. A chain of dependent calls ships in one round trip. The returned JavaScriptPromiseis actually aProxyobject — any method access on it is interpreted as a speculative pipelined call.Proxyinterception is what turns "write it like local JavaScript" into "speculatively transmitted RPC." (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers) -
Capability-based security:
authenticate()returns an unforgeable session stub. The idiomatic auth pattern on Cap'n Web is a top-levelauthenticate(apiKey)that verifies the key and returns anAuthenticatedSessionobject by reference. The client holds a stub; subsequent protected calls (session.whoami(), etc.) go through that stub and do not need to re-present the API key. It is "impossible for the client to forge a session object. The only way to get one is to call authenticate(), and have it return successfully." Cap'n Web specifically cites this as the cleanest answer to an old WebSocket pain point: WebSockets can't carryAuthorizationheaders after the initial handshake, so auth state has traditionally had to live in-band via special "authenticate" messages that mutate connection state. Capability returns replace that with a type-safe handle. (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers) → patterns/capability-returning-authenticate -
Bidirectional by design. The protocol is symmetric — there is no privileged "client" role. Each side exports its "main" interface as export ID 0 at connection start; typically the server's ID 0 is its public API surface and the client's is empty, but either side can export anything and invoke anything. Passing a callback function to an RPC gives the recipient a stub that, when invoked, makes an RPC back to the caller — so e.g. a server can stream updates to a client by invoking a callback the client passed in. Cap'n Web explicitly names this as more expressive than almost every other RPC system. (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers) → concepts/bidirectional-rpc
-
Wire format is JSON with escape-array encoding. The underlying serialization is plain JSON, post-processed to handle non-JSON types. Arrays are treated as escape sequences:
["date", 1758499200000]evaluates to aDate;[["Alice","Bob","Carol"]](double-wrapped) evaluates to the literal inner array. An array whose first element is a type name evaluates to an instance of that type; remaining elements are parameters. Only a fixed set of types is supported — essentially "structured clonable" types plus RPC stub types. You cantail -fa Cap'n Web session and understand it, which is a deliberate departure from Cap'n Proto's zero-copy binary wire format. The design rationale is the browser target: devtools and users alike work with JSON natively. (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers) -
Three built-in transports out of the box. WebSocket (long- lived bidirectional), HTTP batch (short burst — once you
await, the batch is done and all its references are broken — but you can make many calls in one batch and they still ride one round trip if chained via pipelining), andpostMessage()(for iframe + Web Worker communication). Applications can define their own transports — anything that provides a bidirectional message stream works. (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers) -
The
.map()trick: record-replay DSL over promised arrays. This is the feature that makes Cap'n Web a serious alternative to GraphQL for the "list-then- for-each" case ("list the user's friends, and then for each one, fetch their profile photo").promise.map(callback)on a promised array appliescallbackper element — but the callback must be synchronous and the transformation executes server-side, not via a per-element round trip back to the client. How? On the client, the implementation runs the callback once against a placeholderProxyand records the pipelined calls the callback makes. The recording is not JavaScript source — it's a list of pipelined RPC expressions restricted to a non-Turing-complete DSL. And because promise pipelining is what the RPC protocol already represents, "the 'DSL' used to represent 'instructions' for the map function is just the RPC protocol itself." 🤯 The instructions are shipped to the server, which replays them per array element. No round trips, no schema. (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers) → patterns/record-replay-dsl -
Cap'n Web as GraphQL alternative. GraphQL's headline benefit was flattening REST's waterfalls by letting the client request nested data in one query. Cap'n Web solves the same waterfall problem without a new language or ecosystem — just write code in JavaScript, the pipelining happens at the RPC layer. Post argues GraphQL's trade-offs are (a) "new language and tooling" (schema DSL + servers + client libraries), (b) "limited composability" (declarative queries awkward for chained mutations — e.g. "create a user, then immediately use that new user object to make a friend request, all-in-one round trip" is hard in GraphQL, trivial in Cap'n Web), and (c) "different abstraction model" (you learn a new mental model instead of extending JavaScript). Cap'n Web's counter- pitch: "you just call methods and pass objects around, like you would in any other JavaScript code." (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers)
-
Production use: Wrangler remote bindings. Cap'n Web is already the basis of the remote bindings feature in Wrangler (GA 2025-09-16), shipped ~6 days before this announcement post. Local
workerdtest instances speak Cap'n Web RPC to services running in production, so local dev hits real data without emulation. Cloudflare also states "experiments in various frontend applications — expect more blog posts on this in the future." Published 2025-09-22; 643 points on Hacker News (item 45332883). (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers)
Systems named¶
- Cap'n Web — the subject. Pure-TypeScript RPC library, MIT, <10 kB minify+gzip, no deps. Ships HTTP batch
- WebSocket +
postMessage()transports. Workers integration vianewWorkersRpcResponse(request, new MyApiServer()). - Cap'n Proto — spiritual ancestor.
Same capability + pipelining model, schema-based, binary zero-
copy wire format. Named by Varda (author of both) as the
lineage; explicitly does not support
.map()over promised arrays — that's a Cap'n Web innovation. - Cloudflare Workers — first-
class Cap'n Web server target via
newWorkersRpcResponse(…). Workers' existing "JavaScript-native RPC system" is cited as the no-schema design precedent for Cap'n Web. - GraphQL — framed as the previous-generation solution to the waterfall problem; Cap'n Web pitches itself as the simpler no-new-language alternative.
- gRPC — implicit contrast: schema-first, HTTP/2, no capability / no pipelining. Not mentioned by name but is the ecosystem baseline Cap'n Proto + Cap'n Web position themselves against.
- Wrangler — mentioned as the first consumer (remote bindings, GA 2025-09-16). No dedicated wiki page.
Concepts surfaced¶
- concepts/object-capability-rpc — NEW. The "Cap'n" model: functions and objects pass by reference; holding a stub is the authority to invoke that object. Security flows from what references you possess, not from re-checking identity on every call.
- concepts/promise-pipelining — NEW. Using the result of an in-flight RPC as the argument/receiver of dependent calls before the first call has returned, collapsing a chain of dependent calls into one network round trip.
- concepts/bidirectional-rpc — NEW. Symmetric protocol where either side can invoke the other's exported interfaces; passing a function over RPC gives the recipient a stub that calls back to the origin.
- concepts/json-serializable-dsl — the
.map()"instructions" DSL is "just the RPC protocol itself" — the same JSON wire format serves as both RPC messages and the transformation DSL shipped to the server. - concepts/network-round-trip-cost — the entire pitch of
promise pipelining +
.map()is round-trip elimination. - concepts/unified-interface-schema — Cap'n Web uses TypeScript interfaces as the single schema, no separate DSL.
Patterns surfaced¶
- patterns/capability-returning-authenticate — NEW.
authenticate(apiKey)returns an authenticated session stub rather than setting connection state; subsequent protected calls go through the stub. Unforgeable at the protocol layer because the client cannot synthesize a stub — the only way to obtain one is a successfulauthenticate(). - patterns/record-replay-dsl — NEW. Execute a user-
supplied synchronous lambda once against a
Proxyplaceholder on the client; intercept every pipelined method call as a recorded instruction; ship the recording to the server to replay per array element. Works because promise pipelining is itself the DSL. - patterns/typescript-as-codegen-source — Cap'n Web is the
TypeScript-as-schema precedent Cloudflare's 2026-04-13
cfCLI post explicitly cites.
Operational numbers¶
- <10 kB: Cap'n Web minified + gzipped. Zero dependencies.
- One network round trip for chained dependent calls via
promise pipelining +
.map()over promised arrays. - 4 message types on the wire:
push,pull,resolve,reject. - 643 Hacker News points; announcement published 2025-09-22.
- GA 2025-09-16 for Wrangler remote bindings (first production consumer), ~6 days before the announcement post.
- Code-size example: client setup is one line (
newWebSocketRpcSession("wss://example.com/api")); server is anRpcTargetsubclass + one-linefetchhandler wrapping it innewWorkersRpcResponse(…).
Caveats¶
- Experimental. Cloudflare explicitly flags "still new and experimental… a willingness to live on the cutting edge may also be required."
- No runtime type checking. TypeScript is compile-time only; a malicious client can still send wrong-shaped arguments. Cap'n Web suggests pairing with a runtime validator like Zod at the handler boundary; "we hope to add type checking based directly on TypeScript types in the future."
.map()lambda must be synchronous. Anyawaitinside the callback breaks the record-replay capture. Users will hit this in practice and have to restructure their code. Not flagged in the post but a real ergonomic sharp edge.- HTTP batch semantics are strict. Once you
awaitany call in a batch, "the batch is done, and all the remote references received through it become broken" — must start a new batch for subsequent work. - Capability lifetime / disposal is not walked through. A stub held by a remote peer pins the underlying object; IDs are never reused over a connection, but the post doesn't detail the GC / dispose discipline for long-lived WebSocket sessions.
- Only structured-clonable types + stubs serialize. No custom class instances by value; everything else must go by reference (stub) or be representable as structured-clonable data.
- Post-only coverage of production scale. No disclosed throughput / latency / adoption numbers beyond "basis of Wrangler remote bindings" + "experiments in frontend apps."
Source¶
- Original: https://blog.cloudflare.com/capnweb-javascript-rpc-library/
- Raw markdown:
raw/cloudflare/2025-09-22-capn-web-a-new-rpc-system-for-browsers-and-web-servers-e421f854.md - Protocol spec: github.com/cloudflare/capnweb/blob/main/protocol.md
- Repo: github.com/cloudflare/capnweb (MIT)
- HN discussion: news.ycombinator.com/item?id=45332883
Related¶
- systems/capnweb — the canonical system page.
- systems/capnproto — the schema-based ancestor.
- systems/cloudflare-workers — first-class server target.
- concepts/object-capability-rpc / concepts/promise-pipelining / concepts/bidirectional-rpc — the three load-bearing concepts this post introduces to the wiki.
- patterns/capability-returning-authenticate / patterns/record-replay-dsl — the two load-bearing patterns.
- patterns/typescript-as-codegen-source — Cap'n Web is the precedent Cloudflare's 2026-04-13
cfCLI post cites. - companies/cloudflare