Monday, November 9, 2026

gRPC over DataChannels 0.40.0: tracing (beta), Rust proxy, browser-safe SCTP

0.40.0 of grpc-over-datachannels is out. The headline is distributed tracing (beta) that works the same way in every generated runtime — 15 native languages plus the browser — but the release also carries an SCTP delivery overhaul and a measurement about browser packet sizes that changed how every stack in the project sends data.

Release notes and downloads · Browser code generator

Tracing that follows the call

Register a span observer and every generated client and server emits spans with durations and the W3C sampled flag. Unsampled trace contexts still propagate — they just don't record. The part that matters in practice: nested calls parent automatically. When a handler makes its own outbound gRPC call, that call joins the inbound trace with no manual context plumbing (ambient context on native runtimes, explicit trace-context binding in the browser). A client-originated call starts a root span when there is no parent, and the TCP/DataChannel proxy emits its own span for proxied calls, so a trace survives the hop from a stock HTTP/2 client into a DataChannel service.

Every language ships a TRACING.md next to its TESTS.md, and the test suites assert the full client/server parent chain — including the Wasm matrix, which pairs native and facade browser runtimes in both directions and checks that server spans parent to client spans in the same trace. Two real bugs fell out of that coverage: span-id seeding that trapped on wasm32-unknown-unknown, and a Lua span clock that broke in hosts without the os library. Both are fixed and now covered by checks that run against the published packages.

It's designated beta behind one central flag in the generator; the runtime stays dormant until your application registers a tracer.

The 2048-byte wall

While adding large-message legs to the browser matrix, we found something worth sharing: Chromium silently drops SCTP packets larger than 2048 bytes. A 2028-byte packet was acked; a 2128-byte packet was retransmitted forever — no error, no close, just a call that never completes. Chromium's own outgoing datagrams top out around 1225 bytes.

Our three hand-rolled SCTP stacks (Rust, Go, JS) now fragment at 1152-byte payloads: one chunk per packet, the whole packet comfortably inside the datagrams browsers themselves emit, and no IP fragmentation on real network paths. Receivers were always size-agnostic; this is purely a sender-side budget, and it's now enforced everywhere with the measurement recorded in the code.

Alongside that, all three stacks gained RTT-measured retransmit timers (SRTT/RTTVAR feeding the RTO, replacing a fixed 1-second recovery), O(1)-amortized reassembly, and a truthful advertised receive window that shrinks for gap-buffered data. The proxy stress leg grew to 8 MB; the browser matrix now pushes 192 KiB unary calls (just under the RTCDataChannel single-message cap) through 20/20 runtime pairs in Chromium and Firefox. A paced throughput bench (make bench-throughput) baselines our JS stack against Chromium's native one.

Also in 0.40.0

  • A dependency-free Rust TCP/DataChannel proxy in both directions: stock HTTP/2 clients call a grpc-dc server, grpc-dc clients call a stock HTTP/2 backend, TLS on both sides, compression terminated in the proxy. A new malformed-input corpus for the JS ingress proxy (garbage prefaces, truncated frames, gzip bombs, mid-message resets) found and fixed a status-mapping bug.
  • Embedded: the raw-Ethernet Echo proof now fragments a 10 KiB call across ~7 Ethernet frames each way and streams over the no-IP transport.
  • Fresh-consumer gates run against the published packages, so packaging regressions get caught outside the repo.

Packages for this release are live on npm (grpc-dc, kafka-grpc, eventbus-grpc), crates.io, pub.dev, NuGet, Maven Central, LuaRocks, and JSR, with generated runtimes for Rust, Go, JS, Python, Java, C#, C++, Swift, Kotlin, Dart, Zig, Lua, Ruby, PHP, Scala, and C. First published on dev.to. If you try the tracing beta, we'd like to hear where the model chafes — it's the last big surface we expect to iterate on before calling it stable.

Saturday, October 31, 2026

WebRTC-Direct examples in every language: no signaling, plus TURN and STUN modes

The latest grpc-over-datachannels release ships WebRTC-Direct examples for every supported language — generated, like the runtimes themselves. A server listens on a UDP socket and prints a certhash multiaddr (/ip4/…/udp/…/webrtc-direct/certhash/f…); a client dials that address and gets a DTLS/SCTP data channel carrying plain gRPC frames. No signaling server, no SDP exchange.

Three ways to connect

Direct — the client dials the multiaddr straight over UDP. This is the LAN and public-host path, and the server prints everything a client needs on one MULTIADDR line.

Via TURN — a client behind a symmetric NAT allocates a relay on a TURN server and dials the same listener through it. TURN is purely a client-side tool here: the server never changes and never learns TURN exists. Set DIRECT_TURN=host:3478 (plus user/password) or pass --turn to the sidecar.

Full ICE with STUN — a Direct listener gathers no ICE candidates, so STUN belongs to the peer mode: the Go and Rust examples include peer-serve/peer-dial modes where GRPC_DC_STUN=stun.l.google.com:19302 enables server-reflexive candidate gathering, with SDP exchanged through files as a stand-in for any signaling channel. Browsers join through this same full-ICE path — a browser has no UDP sockets, so it can never dial a multiaddr directly.

Every language, one shape

JavaScript, Go, and Rust embed the WebRTC stack and run the examples natively. The other thirteen languages — Python, Ruby, PHP, Lua, C, C++, Dart, Java, Kotlin, Scala, Swift, C#, and Zig — reach WebRTC-Direct through their generated SidecarTransport plus the small Rust direct_sidecar bridge, so the language code is identical whether the path is direct or TURN. Each example has serve and dial modes, and the compose file in examples/direct wires servers and clients together with a shared /signal volume as the whole client→server handoff, coturn fixture included.

Try it

Everything lands in examples/direct in the upcoming v0.36.0 release (December 1): start the Node server, copy its multiaddr, and dial it from any language. The per-language catalog lives at browser-grpc/examples. Two honest limits, documented in the README: a NATted listener still needs a port forward, a public host, or the TURN dial — STUN alone cannot make an inbound UDP listener reachable — and browser + tunnel + TURN is not supported, because the quick tunnels are TCP-only.

Friday, October 30, 2026

v0.35.12: the event bus gets its own repository

Release v0.35.12 of grpc-over-datachannels is out, and its headline is organisational: the generated event bus now has a dedicated home at browser-grpc/eventbus, the same treatment the Kafka proxy got with browser-grpc/kafka-proxy.

What is in the repo

The schema (proto/events.proto, grpcdc.events.v1.EventBus), the complete dependency-free Python and JavaScript trees — broker, clients, demos, and tests that run as-is — and the generated EventBus stubs, brokers, demos, and tests for the other fourteen languages under gen/<lang>/, ready to drop into the main repo's runtime trees. An EVENTBUS.md guide covers the service surface: typed routing tags, containment filters, broker-assigned sequence numbers, and the git-backed log variant.

Run it

The demo ships as a container: docker run --rm ghcr.io/browser-grpc/eventbus-demo:latest starts a broker, subscribes with a topic filter, and publishes three events so you can watch the filter admit two of them. The repo's CI builds the test image and runs the Python and JavaScript suites plus both demos on every push — it is green on the release commit.

Everything else moved too

As usual the release is published across GitHub, npm (grpc-dc), PyPI, NuGet (GrpcDc), Maven Central (io.github.browser-grpc:grpc-dc), and GHCR, all at 0.35.12. Background on the design is in A generated event bus; the main project remains browser-grpc/grpc-over-datachannels.

A generated event bus: publish/subscribe as a gRPC service in sixteen languages

Most systems eventually grow an event bus, and most event buses arrive as a heavyweight broker plus a client library per language. grpc-over-datachannels takes the generator's route instead: define the bus once as a gRPC service — grpcdc.events.v1.EventBus — and generate the broker and clients in all sixteen languages from that one schema.

The whole API is two calls

Publish takes routing tags, an opaque payload (typically your own serialized event proto), and a publisher id, and returns a broker-assigned sequence number. Subscribe takes a containment filter and returns a stream of events. An event carries its tags, payload, the broker-global sequence (1-based, in publish order), publisher id, and the broker's wall-clock timestamp.

Tag-value filtering

Tags are typed values — string, int64, double, bool, or any enum carried by number so it survives peers that do not know the enum. A subscription filter is a list of (tag, value) pairs, and an event is delivered when its tag list contains every pair in the filter; an empty filter receives everything, and the same tag may appear several times when an event must carry several values. There is no query language to learn and no stringly-typed topic hierarchy to govern: matching is set containment, evaluated the same way in every generated implementation.

Why generated matters here

Because the bus is just a .proto, publishers and subscribers can be browser tabs, native services, or mixed pairs over a DataChannel, and the broker itself has a generated implementation per language — including a git-backed event log variant for durable, inspectable history. Sequence numbers make ordering claims testable, and the same generated BDD scenarios that grade the runtimes grade the bus. If your architecture needs events, you already have the client: it was generated with your other stubs.

Update (v0.35.12): the event bus now has a dedicated repository — browser-grpc/eventbus — with the schema, the full Python and JavaScript trees, generated sources for the other fourteen languages, and CI. Run the demo with docker run --rm ghcr.io/browser-grpc/eventbus-demo:latest.

One open port for every peer: PeerMux and single-socket WebRTC serving

Running a WebRTC server usually means socket sprawl: a listener per peer, or a framework that owns your sockets for you. grpc-over-datachannels takes the opposite approach with PeerMux: many signaled peers hosted on one bound UDP socket — the single-port topology popularised by large conferencing systems — implemented in the Rust, Go, and JavaScript (Node) stacks.

Routing packets without addresses

A mux cannot route by source address: peers sit behind NATs, addresses get reused, and two muxes may peer with each other. So PeerMux routes by protocol contents instead. STUN packets route by ICE ufrag. The first DTLS flight routes by a pair-hint extension. Every later DTLS record routes by its RFC 9146 connection id. Once the DataChannel is up, the gRPC frames never see the socket at all — the mux is purely a packet demultiplexer under the stack.

What falls out of the design

A server needs exactly one open port for all its peer connections, which simplifies firewalls, containers, and home hosting. TURN still works on muxed peers (the allocation's UDP leg rides a dedicated per-peer socket). And because routing never depends on who sent a packet from where, two muxes can peer with each other directly.

Verified cross-stack

One generated scenario tests all three stacks, and the cross-stack matrix pairs mux against mux across implementations — Rust serving Go, Go serving JavaScript, and so on — so the routing rules are graded against independent code, not just against the implementation that proposed them. If you are embedding WebRTC serving into a native process, one port is all you have to give it.

WebRTC with no operating system: the embedded profile

WebRTC is usually something that happens on phones, browsers, and servers — places with an operating system, a network stack, and memory to spare. But a gRPC-speaking sensor or controller should not need Linux just to make an encrypted call. The WebRTC stack under grpc-over-datachannels is built no_std for exactly that reason.

Traits instead of an OS

The Rust stack cross-builds for Cortex-M. Everything an OS would normally provide is injected through platform traits: a clock, an entropy source, and the datagram transport. On top of those traits sits an in-crate network stack — Ethernet framing, ARP, IPv4/IPv6, UDP, and DHCP — so the device does its own networking from the wire up: plug in Ethernet, get an address, run ICE/DTLS/SCTP, open a DataChannel, speak gRPC.

Proving it without hardware on your desk

The bring-up is demonstrated by a two-device example over a virtual cable (cargo run --example iot_cable in webrtc/rust): power-on to DataChannel echo between two stack instances with nothing between them but the simulated wire. That same frame protocol then carries generated gRPC stubs — the small C99 and Lua runtimes target constrained hosts too, and every generated runtime is dependency-free, which matters when there is no package manager to lean on.

Why bother

Because the alternative for embedded RPC is usually a bespoke protocol you now own forever. A generated stub, a standard wire format, and interoperability graded against browsers and stock gRPC implementations give an embedded target the same contract as every other peer in the system — including calling, and being called by, a browser tab.

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.