Skip to content

CLOUDFLARE 2025-09-22

Read original ↗

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

  1. 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 .capnp schema language + codegen step — you declare an API as a plain TypeScript interface, implement it by extending a marker class RpcTarget, 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)

  2. 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 actually awaits), 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)

  3. Promise pipelining is enabled by predictable export IDs. Because the caller can predict the positive ID each push will 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 JavaScript Promise is actually a Proxy object — any method access on it is interpreted as a speculative pipelined call. Proxy interception 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)

  4. Capability-based security: authenticate() returns an unforgeable session stub. The idiomatic auth pattern on Cap'n Web is a top-level authenticate(apiKey) that verifies the key and returns an AuthenticatedSession object 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 carry Authorization headers 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

  5. 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

  6. 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 a Date; [["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 can tail -f a 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)

  7. 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), and postMessage() (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)

  8. 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 applies callback per 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 placeholder Proxy and 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

  9. 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)

  10. 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 workerd test 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 via newWorkersRpcResponse(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 successful authenticate().
  • patterns/record-replay-dsl — NEW. Execute a user- supplied synchronous lambda once against a Proxy placeholder 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 cf CLI 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 an RpcTarget subclass + one-line fetch handler wrapping it in newWorkersRpcResponse(…).

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. Any await inside 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 await any 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

Last updated · 200 distilled / 1,178 read