Friday, October 30, 2026

One data channel, many calls: the grpc-dc frame protocol

gRPC normally rides on HTTP/2: one TCP connection, many streams, HPACK headers, flow-control windows. WebRTC DataChannels already give us a reliable, ordered, encrypted message pipe — so grpc-over-datachannels replaces the HTTP/2 layer with a small frame protocol designed for that pipe. This post is a tour of the wire format, specified in SPEC.md in the repository.

One message, one frame

DataChannels preserve message boundaries, so every binary message is exactly one frame: a type byte, a 32-bit big-endian stream id, and a payload. The frame types are deliberately few. HEADERS opens a call and carries a small JSON document — the method path and metadata — instead of an HTTP header block. DATA carries exactly one serialized protobuf message, untouched. HALF_CLOSE says the client will send no more, mirroring gRPC semantics. TRAILERS ends the call with status, and RST aborts from either side.

Stream lifecycle

The initiating peer allocates odd stream ids (1, 3, 5, …), the other side even ones, so both peers can open calls without coordination. A full-duplex call is: HEADERS from the client, HEADERS back with initial metadata, any number of DATA frames, HALF_CLOSE, more DATA, then TRAILERS. A unary call is just the degenerate case of one DATA frame each way — there is no separate unary fast path to keep in sync across sixteen runtimes.

The details that matter

Deadlines travel as the familiar grpc-timeout metadata key, recomputed at each forwarding hop. Unknown frame types are ignored, which gives the protocol forward compatibility without a version negotiation. Malformed frames, unknown stream ids, and calls to unregistered methods fail closed with an error status. And because a data channel is a stream of messages rather than bytes, backpressure is handled per stream at the frame level rather than by parsing a byte stream.

The full specification, including tracing propagation and the deliberate divergences from gRPC-over-HTTP/2, lives in the grpc-over-datachannels repository. Try the frames live in the in-browser generator.

Rebuilding WebRTC from scratch: ICE, DTLS, SCTP with zero dependencies

Every language runtime in grpc-over-datachannels is generated and dependency-free — but a gRPC client in Go or Rust still needs something underneath that speaks WebRTC to a browser. Native WebRTC libraries are large, stateful, and different in every language. So the project reimplements the WebRTC stack itself: ICE for connectivity, DTLS for encryption, SCTP for the data-channel transport, and DCEP for channel setup. This post explains the profile we implemented and the rules that kept it tractable.

A profile, not the whole RFC stack

WebRTC is a family of specifications with many optional corners. The reimplementation targets a profile: the subset real browsers actually negotiate, verified continuously against Chromium and Firefox (driven headlessly through Playwright) rather than against our reading of the RFCs alone. Where browsers diverge from the documents, the browsers win, and the divergence is written down in the spec.

What is in the stack

STUN/TURN for NAT traversal (with per-language tests against public STUN servers in CI), ICE with host/server-reflexive/relay candidates, DTLS 1.2 handshakes, SCTP with ordered-reliable and lossy/unordered channel modes, and a single-socket multiplexer (PeerMux) that hosts many peers on one bound UDP port. WebRTC-Direct gives native peers a libp2p-style direct dial without SDP file shuffling. The Rust stack is no_std: platform traits inject clock, entropy, and datagrams, and an in-crate network stack provides Ethernet framing, ARP, IP, UDP, and DHCP, so the same code runs on embedded targets with no operating system.

Why zero dependencies is the point

A generated client you paste into a project should not drag in a dependency tree you have to audit. The stack builds from each language's standard library only, which is also what makes the same wire behaviour reproducible across the Go, JavaScript, and Rust stacks: they are checked against each other and against real browsers, not against a shared library's quirks. The design rules and verification strategy are in SPEC-WEBRTC.md in the repository.

Trust but verify: oracle-testing generated gRPC against Google's implementations

Code generation has a trust problem: if the generator and its tests share an assumption, the tests pass and the code is still wrong. grpc-over-datachannels generates its gRPC runtimes and most of its tests from one source of truth, which makes that failure mode easy to fall into. The defence is an oracle: stock, Google-maintained gRPC implementations that never saw our generator.

The bridge

A TCP-to-DataChannel proxy connects the two worlds. On one side it speaks ordinary HTTP/2 gRPC over TCP, exactly what grpc-java and grpc-go expect. On the other it speaks the grpc-dc frame protocol over a WebRTC DataChannel. A generated client can therefore call a stock server, and a stock client can call a generated server, with every frame crossing both wire formats on the way.

What gets graded

The interop matrix runs the standard gRPC test vocabulary — unary, server-streaming, client-streaming, bidirectional streaming, empty calls, large messages, status codes, metadata, and deadlines — across generated clients and servers in JavaScript, Go, Rust, Python, and the other generated languages, against the reference implementations as the source of truth. The same generated scenario table (the BDD suite) also runs natively per language and inside real Chromium, Firefox, and WebKit browsers via Playwright, so a behaviour has to be right in three independent venues before it counts.

Why this matters more than unit tests

Unit tests verify our code against our understanding. The oracle verifies our understanding against the ecosystem's. Every divergence the proxy runs surfaced — in framing, trailers, timeout formats, or stream teardown — was a place the specification in our heads differed from the one on the wire, found before a user could find it for us. If you generate protocol code, budget for an oracle: it is the only test suite that cannot agree with your mistakes.

Inside kafka-proxy: consumer groups, rebalances, and offset commits over gRPC

kafka-proxy exposes Apache Kafka as a generated gRPC service, grpcdc.kafka.v1.Kafka, so any grpc-over-datachannels client — including a browser tab — can produce and consume without a Kafka client library. The interesting engineering is not the produce path; it is consumer groups, where Kafka's protocol is a distributed-systems negotiation. This post walks through how the proxy holds that state.

Sessions live on the proxy

Kafka group membership is stateful: joins, syncs, heartbeats, offset commits, and generation ids that fence off zombie members. Browser and thin native clients should not each reimplement that, so producer and consumer-group sessions live on the proxy. A client opens a stream and subscribes; the proxy joins the group, distributes assignments, and heartbeats on the client's behalf, against both Apache Kafka (KRaft) and Redpanda.

Rebalances are the hard part

When group membership changes, the coordinator bumps the generation and in-flight commits from the old generation are rejected. The proxy treats that as weather, not failure: broker connections reconnect once after transport errors, offset commits retry inside the session window until the new generation accepts them, and members rejoin without tearing down the client's stream. The end-to-end suite (21 tests per broker) races commits against forced rebalances precisely because that interleaving is where offsets get lost.

Restart resume

The newest test replaces the entire proxy process mid-group: a new instance comes up, the group reforms, and consumption resumes exactly at the committed offset — no redelivery storm, no skipped records. That property is what makes the proxy deployable as a stateless-ish sidecar in front of a broker: run it as ghcr.io/browser-grpc/kafka-proxy, point it at Kafka or Redpanda, and generated clients in sixteen languages can treat Kafka as just another gRPC service.

grpc-dc everywhere: npm, PyPI, NuGet, Maven Central, GHCR, and Go modules

Generated code is only useful if it drops into the package manager you already use. With release v0.35.10, grpc-dc — gRPC over WebRTC DataChannels, with generated dependency-free runtimes — is published across the registries:

Every artifact is generated from the public repositories — browser-grpc/grpc-over-datachannels and browser-grpc/kafka-proxy — under Apache-2.0, and code generated from your own .proto files is yours without restriction. Public CI in both repos runs the trimmed exported test suites in generated Dockerfiles, so you can reproduce exactly what the registries received.

New here? Start with the live in-browser generator: paste a .proto, pick languages, download stubs and runtimes — no install. Documentation is at browser-grpc.github.io/grpc-over-datachannels. More launch notes, design posts, and release write-ups will follow on this blog.

Kafka over gRPC: a Rust Kafka proxy with generated clients for browser and native

Our second launch is kafka-proxy: a Rust proxy that exposes Apache Kafka through a generated gRPC service, grpcdc.kafka.v1.Kafka, so any generated grpc-over-datachannels client — browser or native — can produce to and consume from Kafka without a Kafka client library in that language.

The proxy terminates generated gRPC on one side and speaks the real Kafka protocol on the other, to both Apache Kafka (KRaft) and Redpanda. Producer sessions and consumer-group sessions live on the proxy side: clients open streams, publish records, subscribe, and commit offsets through gRPC calls, while the proxy handles broker connections, group membership, and rebalances. Broker connections reconnect after transport failures, and offset commits retry through generation churn, so a rebalance does not silently lose progress.

Coverage

The proxy is exercised by an extensive generated end-to-end suite — 21 tests per broker — covering produce/consume round trips, consumer groups, offset commits across rebalances, and resuming a group after the proxy itself restarts. The same suite runs against Apache Kafka and Redpanda in CI, in generated Dockerfiles that readers can run themselves.

Run it

Code, guide, and CI live at browser-grpc/kafka-proxy. A prebuilt container image is published as ghcr.io/browser-grpc/kafka-proxy, and the main project is at browser-grpc/grpc-over-datachannels. If you can call gRPC from your language — including from a browser tab — you can now reach Kafka through it.

For the internals — how producer and consumer-group sessions, rebalances, and offset commits are held on the proxy — see Inside kafka-proxy: consumer groups, rebalances, and offset commits over gRPC.

Introducing grpc-over-datachannels: gRPC over WebRTC DataChannels, without HTTP/2

Today we are launching grpc-over-datachannels: gRPC running over a single WebRTC RTCDataChannel instead of HTTP/2. Many concurrent calls — unary, server-streaming, client-streaming, and bidirectional streaming — are multiplexed onto one ordered, reliable data channel using a small frame protocol, with no HTTP/2 stack and no server endpoint to reach.

The runtimes are generated and dependency-free. From one source of truth, recursive metaprogramming generates the gRPC runtime and protoc plugins for Rust, JavaScript, Go, Python, Java, C#, C++, Swift, Kotlin, Dart, Zig, C99, Lua, Ruby, PHP, and Scala — for native clients and servers, and for browser clients (including WASM facades that wrap the JavaScript browser client). Generated tests verify the wire protocol against stock gRPC implementations through a TCP-to-DataChannel proxy, so behaviour is checked against the reference, not just against itself.

Try it in the browser

No install is needed to generate stubs for your own service. Open the live in-browser generator, paste or edit a .proto file, pick your languages, and download generated stubs plus the matching runtimes. Schema compilation and the actual code generators run entirely client-side in the page.

Get the code

Source, specification, quick start, and examples are on GitHub: browser-grpc/grpc-over-datachannels. Documentation lives at browser-grpc.github.io/grpc-over-datachannels. Current release: v0.36.0.

This blog is now the project log for grpc-over-datachannels. The older low-latency and systems posts below — on websockets, AF_XDP, kernel latency, and socket multiplexing — are the background this work grew out of, and they are staying. Next up: the Kafka proxy, and the package ecosystem.

Dig deeper

The wire format under all of this is specified in One data channel, many calls: the grpc-dc frame protocol. Underneath it sits our own WebRTC stack: Rebuilding WebRTC from scratch: ICE, DTLS, SCTP with zero dependencies. And the generated runtimes are graded against Google's implementations, not just themselves: Trust but verify: oracle-testing generated gRPC.