Skip to content

CONCEPT Cited by 1 source

Bidirectional RPC

What it is

Bidirectional RPC is an RPC model in which the protocol is symmetric — there is no privileged "client" and "server" at the protocol layer. Either peer can invoke methods on the other's exported interfaces; passing a function or object over RPC gives the recipient a stub whose invocations route back to the peer that created it.

Common shapes:

  • A client passes a callback to a server method; the server later invokes the callback to stream updates back.
  • A server returns an object reference; the client invokes methods on that object, and each invocation reaches back to the server where the object lives.
  • Two browser windows exchange postMessage() stubs and call each other's APIs as equals.

This is a direct consequence of object-capability RPC treating functions and objects as first- class wire values — once references go both ways, calls do too.

Why it matters

  • Callbacks become a natural primitive. In a classical RPC (gRPC, JSON-RPC, Thrift), streaming updates from server to client requires a dedicated streaming call type; in bidirectional RPC, the client simply passes a callback function and the server calls it. No protocol extension required.
  • WebSocket friendliness. WebSockets are inherently bidirectional message streams, but their APIs don't support headers / cookies after the initial handshake. A symmetric RPC protocol fits them natively — auth can be done in-band via a capability-returning method and subsequent messages flow both ways. See the WebSocket auth note in patterns/capability-returning-authenticate.
  • Works across any bidirectional transport. Cap'n Web ships HTTP batch (request-scoped), WebSocket (long-lived), and postMessage() (in-process, browser) out of the box — and the same protocol semantics apply to all three because they all provide a bidirectional message stream.
  • Enables peer-to-peer patterns. Two collaborating services can expose capabilities to each other without one being nominally "the server" — useful for symmetric microservices meshes and for browser-to-browser (via a relay) scenarios.

Canonical mechanics (Cap'n Web)

At connection start, both peers populate their export tables with a single entry at ID 0 representing their "main" interface. Typically the server exports its public API surface as ID 0 and the client exports an empty interface — but either side can export whatever it wants. Subsequent exports are added in two ways:

  1. When Peer A sends a message containing a function or object reference, Peer A adds the target to its export table at a negative ID (starting at -1, counting down).
  2. When Peer A sends a push message asking Peer B to evaluate an expression, the result lands in Peer B's export table at a positive ID (starting at 1, counting up) — enabling promise pipelining.

A callback passed from a client to a server is an instance of (1): the client exports the callback at a negative ID; the server receives a stub; invoking the stub sends a push message back the other way, and the "call" flows client ← server → client the same way any other call flows.

Trade-offs

  • Transport must be bidirectional. Naïve request/reply transports (a single HTTP POST with no streaming) cannot support post-hoc callbacks. Cap'n Web's HTTP batch mode explicitly breaks all references once the batch completes for this reason — you can pipeline within the batch, but you cannot retain a stub across batches.
  • Connection-oriented. Each connection has its own export tables; scaling horizontally requires stickiness to the connection's originating server (for WebSockets) or an explicit redistribution mechanism. Contrast stateless gRPC unary calls.
  • Resource management is tricky. A stub held by a remote peer pins the underlying object. Long-lived connections with many callbacks need a disposal discipline (Cap'n Web IDs are never reused within a connection).
  • Firewall / proxy awareness. Pure RPC-over-HTTP deployments that expect outbound-only flow from clients will not see server-initiated calls. Transports that carry both directions (WebSocket, HTTP/2 server push, postMessage, etc.) are required.

Seen in

Last updated · 200 distilled / 1,178 read